SharpMeow commited on
Commit
aa04a26
·
verified ·
1 Parent(s): 70cea09

One page scroll bar and American English

Browse files
Files changed (7) hide show
  1. README.md +1 -1
  2. app.py +18 -11
  3. guide/index.html +3 -3
  4. lasfield.py +8 -8
  5. media/example-result.png +2 -2
  6. media/howto-make.gif +2 -2
  7. onescroll.html +62 -0
README.md CHANGED
@@ -74,7 +74,7 @@ The page is also an API and an MCP server (`https://sharpmeow-lasagna-fields.hf.
74
  `list_scans`, `list_models`, `example_points`, `plan_field` and `make_field`. See the page's **Agents and API** tab.
75
  A field uses the caller's ZeroGPU time.
76
 
77
- ## Credit and licences
78
 
79
  Made by Chase Hendrick. Code: Apache 2.0 (see `LICENSE` and `NOTICE`). Model and code it runs: villa's lasagna authors,
80
  MIT. Scans: Vesuvius Challenge open data, CC BY-NC 4.0; the fields, made from them, are CC BY-NC 4.0 too. Source and
 
74
  `list_scans`, `list_models`, `example_points`, `plan_field` and `make_field`. See the page's **Agents and API** tab.
75
  A field uses the caller's ZeroGPU time.
76
 
77
+ ## Credit and licenses
78
 
79
  Made by Chase Hendrick. Code: Apache 2.0 (see `LICENSE` and `NOTICE`). Model and code it runs: villa's lasagna authors,
80
  MIT. Scans: Vesuvius Challenge open data, CC BY-NC 4.0; the fields, made from them, are CC BY-NC 4.0 too. Source and
app.py CHANGED
@@ -316,7 +316,7 @@ def make_field(volume: str, z: float, y: float, x: float, size: float = 256, mod
316
  size: the box side in voxels of the level the model reads, 256 or 512.
317
  model: a model id from list_models; 'lasagna' is villa's own predict3d.
318
 
319
- Returns a picture (middle slices along z, y and x: CT, cos, grad_mag, normals as colour), the receipt (the
320
  box, the model and its pinned commit and checkpoint, timings, and the agreement with the open data's own field
321
  with a shifted control), and a zip with villa's output folder, field.npz and receipt.json. Uses the caller's
322
  ZeroGPU time. Model output: geometry, not a reading.
@@ -324,7 +324,7 @@ def make_field(volume: str, z: float, y: float, x: float, size: float = 256, mod
324
  p = L.plan(volume, z, y, x, int(size), model=model)
325
  with one_run(request):
326
  r = run_field(p, progress)
327
- c = p["centre_level0_zyx"]
328
  pic = L.compose(r["pictures"], f"{p['sample']} · {p['size_zyx'][0]}-voxel box at z {c[0]}, y {c[1]}, x {c[2]}")
329
  return pic, r["receipt"], r["zip"]
330
 
@@ -361,7 +361,7 @@ says which way the sheet faces at each point. villa's lasagna surface fit and th
361
 
362
  ![The CT of one box of PHerc0139 next to villa's lasagna field, stepping through the box](/gradio_api/file=media/howto-slices.gif)
363
 
364
- *PHerc0139 (not on any prize list), one box about 2.5 mm a side: the CT, cos and the normal as colour, stepping
365
  through z. Model output: geometry, not a reading of any text.*
366
 
367
  <h3 id="first">1. Make your first field (one minute)</h3>
@@ -378,7 +378,7 @@ through z. Model output: geometry, not a reading of any text.*
378
  ![A result: three rows of slices, each row CT, cos, grad_mag and normal](/gradio_api/file=media/example-result.png)
379
 
380
  - **Rows** are the middle slice of your box along z, y and x. **Columns** are the CT, cos, grad_mag and the
381
- normal as colour (red for x, green for y, blue for z).
382
  - **cos** should run along the papyrus in the CT, one bright and one dark band per layer. Where layers touch or
383
  the CT is unclear, the bands bend or merge: that is where surface tracing is hard too.
384
  - **The check** compares your field with the published one for the same box, and with a **control**: the same
@@ -404,7 +404,7 @@ through z. Model output: geometry, not a reading of any text.*
404
 
405
  Boxes are whole 256-voxel blocks of the scan, because villa writes the field in 64-voxel chunks of its quarter
406
  grid and puts its tiles on one lattice over the whole scan: that is why a box's field is the whole-scan field
407
- there. The box is the block nearest your point, so your point is inside it but not always at its centre.
408
 
409
  <h3 id="download">4. Use the download</h3>
410
 
@@ -444,7 +444,7 @@ python scripts/bootstrap_venv.py --venv .venv
444
  ```
445
 
446
  Measured on an NVIDIA L4 (25 Oct 2026): about 2 million voxels a second end to end, so about ten minutes per
447
- cubic centimetre at 9.4 um. It fits in 5.6 GB of GPU memory at its default tile, 2.3 GB at `--tile-size 128`.
448
 
449
  <h3 id="wrong">When something goes wrong</h3>
450
 
@@ -492,8 +492,9 @@ CSS = """
492
  background:repeating-linear-gradient(180deg,#c2462b 0 4px,#e07a3f 4px 7px,#f3e2bf 7px 10px,#4f7a3a 10px 11px,#f3e2bf 11px 12px); }
493
  .lf-head h1 { white-space:nowrap; font-family: Fraunces, Georgia, serif; font-weight:650; font-size:30px; margin:0; letter-spacing:-.01em; }
494
  .lf-head span { color: var(--body-text-color-subdued); font-size:15px; }
495
- .lf-guide iframe { width:100%; height:calc(100vh - 150px); min-height:640px; border:0; border-radius:12px;
496
- background:#f7f2ea; }
 
497
  .lf-note p { font-size:13px; color: var(--body-text-color-subdued); margin:0; }
498
  .lf-est p { font-size:13.5px; color: var(--body-text-color-subdued); }
499
  .lf-point { flex-wrap: nowrap !important; }
@@ -502,6 +503,9 @@ CSS = """
502
  .lf-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 760px); display:block; margin:8px 0; }
503
  #lf-dl .empty { min-height: 64px !important; height: 64px !important; }
504
  #lf-dl .empty .icon { height: 28px; }
 
 
 
505
  .lf-howto { max-width: 1000px; margin: 0 auto !important; }
506
  .lf-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
507
  .lf-howto em { color: var(--body-text-color-subdued); font-size: 13.5px; }
@@ -510,7 +514,6 @@ CSS = """
510
  @media (max-width: 640px) {
511
  .lf-head h1 { font-size:24px; }
512
  .lf-head span { display:none; }
513
- .lf-guide iframe { height:calc(100vh - 120px); min-height:520px; }
514
  #lf-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
515
  }
516
  """
@@ -524,6 +527,9 @@ ten whole scans to download, and any box made on demand. Model output, not a rea
524
  <link rel="preconnect" href="https://fonts.googleapis.com">
525
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
526
  """
 
 
 
527
 
528
 
529
  def theme():
@@ -548,7 +554,8 @@ def guide_html():
548
  import html
549
  page = GUIDE.read_text()
550
  # its pictures (assets/..., built in its script) point at Gradio's file route; a <base> cannot do it, because a
551
- # base of /gradio_api/file= resolves assets/x to /gradio_api/assets/x. Anchors scroll inside the frame
 
552
  page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
553
  fix = ('<style>.wrap{max-width:1480px!important}</style>'
554
  '<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
@@ -644,7 +651,7 @@ def build_demo():
644
  dl = gr.File(label="Download: villa output folder, field.npz, receipt.json", height=88,
645
  elem_id="lf-dl")
646
  with gr.Accordion("Receipt", open=False):
647
- receipt = gr.JSON()
648
  with gr.Accordion("About the model", open=False):
649
  gr.Markdown(ABOUT_MODEL.format(commit=L.VILLA["commit"][:12],
650
  sha=L.WEIGHTS["sha256"][:16] + "…"))
 
316
  size: the box side in voxels of the level the model reads, 256 or 512.
317
  model: a model id from list_models; 'lasagna' is villa's own predict3d.
318
 
319
+ Returns a picture (middle slices along z, y and x: CT, cos, grad_mag, normals as color), the receipt (the
320
  box, the model and its pinned commit and checkpoint, timings, and the agreement with the open data's own field
321
  with a shifted control), and a zip with villa's output folder, field.npz and receipt.json. Uses the caller's
322
  ZeroGPU time. Model output: geometry, not a reading.
 
324
  p = L.plan(volume, z, y, x, int(size), model=model)
325
  with one_run(request):
326
  r = run_field(p, progress)
327
+ c = p["center_level0_zyx"]
328
  pic = L.compose(r["pictures"], f"{p['sample']} · {p['size_zyx'][0]}-voxel box at z {c[0]}, y {c[1]}, x {c[2]}")
329
  return pic, r["receipt"], r["zip"]
330
 
 
361
 
362
  ![The CT of one box of PHerc0139 next to villa's lasagna field, stepping through the box](/gradio_api/file=media/howto-slices.gif)
363
 
364
+ *PHerc0139 (not on any prize list), one box about 2.5 mm a side: the CT, cos and the normal as color, stepping
365
  through z. Model output: geometry, not a reading of any text.*
366
 
367
  <h3 id="first">1. Make your first field (one minute)</h3>
 
378
  ![A result: three rows of slices, each row CT, cos, grad_mag and normal](/gradio_api/file=media/example-result.png)
379
 
380
  - **Rows** are the middle slice of your box along z, y and x. **Columns** are the CT, cos, grad_mag and the
381
+ normal as color (red for x, green for y, blue for z).
382
  - **cos** should run along the papyrus in the CT, one bright and one dark band per layer. Where layers touch or
383
  the CT is unclear, the bands bend or merge: that is where surface tracing is hard too.
384
  - **The check** compares your field with the published one for the same box, and with a **control**: the same
 
404
 
405
  Boxes are whole 256-voxel blocks of the scan, because villa writes the field in 64-voxel chunks of its quarter
406
  grid and puts its tiles on one lattice over the whole scan: that is why a box's field is the whole-scan field
407
+ there. The box is the block nearest your point, so your point is inside it but not always at its center.
408
 
409
  <h3 id="download">4. Use the download</h3>
410
 
 
444
  ```
445
 
446
  Measured on an NVIDIA L4 (25 Oct 2026): about 2 million voxels a second end to end, so about ten minutes per
447
+ cubic centimeter at 9.4 um. It fits in 5.6 GB of GPU memory at its default tile, 2.3 GB at `--tile-size 128`.
448
 
449
  <h3 id="wrong">When something goes wrong</h3>
450
 
 
492
  background:repeating-linear-gradient(180deg,#c2462b 0 4px,#e07a3f 4px 7px,#f3e2bf 7px 10px,#4f7a3a 10px 11px,#f3e2bf 11px 12px); }
493
  .lf-head h1 { white-space:nowrap; font-family: Fraunces, Georgia, serif; font-weight:650; font-size:30px; margin:0; letter-spacing:-.01em; }
494
  .lf-head span { color: var(--body-text-color-subdued); font-size:15px; }
495
+ /* no fixed height: onescroll.html fits the guide's frame to the guide, so the page scrolls and the frame does not;
496
+ min-height only holds the place until it has */
497
+ .lf-guide iframe { width:100%; min-height:640px; border:0; border-radius:12px; background:#f7f2ea; }
498
  .lf-note p { font-size:13px; color: var(--body-text-color-subdued); margin:0; }
499
  .lf-est p { font-size:13.5px; color: var(--body-text-color-subdued); }
500
  .lf-point { flex-wrap: nowrap !important; }
 
503
  .lf-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 760px); display:block; margin:8px 0; }
504
  #lf-dl .empty { min-height: 64px !important; height: 64px !important; }
505
  #lf-dl .empty .icon { height: 28px; }
506
+ /* one page scroll bar: the download's file row and the receipt grow with what they hold, so the page scrolls */
507
+ #lf-dl .file-preview-holder { max-height:none !important; overflow:visible !important; }
508
+ #lf-receipt { max-height:none !important; overflow:visible !important; }
509
  .lf-howto { max-width: 1000px; margin: 0 auto !important; }
510
  .lf-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
511
  .lf-howto em { color: var(--body-text-color-subdued); font-size: 13.5px; }
 
514
  @media (max-width: 640px) {
515
  .lf-head h1 { font-size:24px; }
516
  .lf-head span { display:none; }
 
517
  #lf-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
518
  }
519
  """
 
527
  <link rel="preconnect" href="https://fonts.googleapis.com">
528
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
529
  """
530
+ # one page scroll bar: fits the guide's frame to its content (written by python -m kit spaceconnect render)
531
+ ONESCROLL = HERE / "onescroll.html"
532
+ HEAD += ONESCROLL.read_text() if ONESCROLL.is_file() else ""
533
 
534
 
535
  def theme():
 
554
  import html
555
  page = GUIDE.read_text()
556
  # its pictures (assets/..., built in its script) point at Gradio's file route; a <base> cannot do it, because a
557
+ # base of /gradio_api/file= resolves assets/x to /gradio_api/assets/x. Anchors scroll to their section (the frame
558
+ # is as tall as the guide, so the page scrolls, not the frame)
559
  page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
560
  fix = ('<style>.wrap{max-width:1480px!important}</style>'
561
  '<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
 
651
  dl = gr.File(label="Download: villa output folder, field.npz, receipt.json", height=88,
652
  elem_id="lf-dl")
653
  with gr.Accordion("Receipt", open=False):
654
+ receipt = gr.JSON(max_height=None, elem_id="lf-receipt")
655
  with gr.Accordion("About the model", open=False):
656
  gr.Markdown(ABOUT_MODEL.format(commit=L.VILLA["commit"][:12],
657
  sha=L.WEIGHTS["sha256"][:16] + "…"))
guide/index.html CHANGED
@@ -79,12 +79,12 @@ dl.facts{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0;font-
79
  dl.facts dt{color:var(--ink3)}dl.facts dd{margin:0;word-break:break-word}
80
  .links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
81
  .codebox{position:relative;margin-top:10px}
82
- pre{max-height:340px;background:var(--code);color:var(--codeink);border-radius:10px;padding:14px 16px;overflow:auto;margin:0;line-height:1.55}
83
  pre .c{color:#9a8f7e}
84
  .codebox pre{padding-top:34px}
85
  .copy{position:absolute;top:8px;right:8px;border:1px solid #ffffff22;background:#ffffff14;color:#efe6d6;border-radius:7px;padding:3px 9px;font-size:12px;cursor:pointer}
86
  .copy:hover{background:#ffffff26}
87
- .tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-top:18px}
88
  .tabs button{border:0;background:none;padding:8px 12px;color:var(--ink2);cursor:pointer;font:inherit;font-size:14px;border-bottom:2px solid transparent;margin-bottom:-1px}
89
  .tabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
90
  .detail>*,.region>*,.panel,.codebox,.cards3>*{min-width:0}
@@ -430,7 +430,7 @@ let drag=false;$('map').onpointerdown=e=>{drag=true;mapAt(e)};addEventListener('
430
 
431
  /* copy */
432
  document.addEventListener('click',e=>{const b=e.target.closest('.copy');if(!b)return;
433
- const t=$(b.dataset.copy).textContent;const done=()=>{$('toast').classList.add('on');setTimeout(()=>$('toast').classList.remove('on'),1200)};
434
  if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(done,()=>fallback(t,done));else fallback(t,done)});
435
  function fallback(t,done){const a=document.createElement('textarea');a.value=t;document.body.appendChild(a);a.select();try{document.execCommand('copy');done()}catch(e){}a.remove()}
436
 
 
79
  dl.facts dt{color:var(--ink3)}dl.facts dd{margin:0;word-break:break-word}
80
  .links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
81
  .codebox{position:relative;margin-top:10px}
82
+ pre{background:var(--code);color:var(--codeink);border-radius:10px;padding:14px 16px;overflow-x:auto;overflow-y:hidden;margin:0;line-height:1.55}
83
  pre .c{color:#9a8f7e}
84
  .codebox pre{padding-top:34px}
85
  .copy{position:absolute;top:8px;right:8px;border:1px solid #ffffff22;background:#ffffff14;color:#efe6d6;border-radius:7px;padding:3px 9px;font-size:12px;cursor:pointer}
86
  .copy:hover{background:#ffffff26}
87
+ .tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--line);margin-top:18px}
88
  .tabs button{border:0;background:none;padding:8px 12px;color:var(--ink2);cursor:pointer;font:inherit;font-size:14px;border-bottom:2px solid transparent;margin-bottom:-1px}
89
  .tabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
90
  .detail>*,.region>*,.panel,.codebox,.cards3>*{min-width:0}
 
430
 
431
  /* copy */
432
  document.addEventListener('click',e=>{const b=e.target.closest('.copy');if(!b)return;
433
+ const t=$(b.dataset.copy).textContent;const done=()=>{b.textContent='Copied';$('toast').classList.add('on');setTimeout(()=>{b.textContent='Copy';$('toast').classList.remove('on')},1200)};
434
  if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(done,()=>fallback(t,done));else fallback(t,done)});
435
  function fallback(t,done){const a=document.createElement('textarea');a.value=t;document.body.appendChild(a);a.select();try{document.execCommand('copy');done()}catch(e){}a.remove()}
436
 
lasfield.py CHANGED
@@ -252,19 +252,19 @@ def plan(vid, z, y, x, size=DEFAULT_SIZE, level=None, model="lasagna"):
252
  meta = zarray(v, level)
253
  shape = [int(s) for s in meta["shape"]]
254
  f = 2 ** level
255
- centre = [int(round(float(c))) for c in (z, y, x)]
256
- for c, n in zip(centre, base["shape"]):
257
  if not 0 <= c < n:
258
- raise FieldError(f"the point {centre} is outside the scan (level-0 shape {base['shape']})")
259
  origin = [max(0, min((c // f - size // 2 + ALIGN // 2) // ALIGN * ALIGN, (n - 1) // ALIGN * ALIGN))
260
- for c, n in zip(centre, shape)]
261
  box_size = [min(size, n - o) for o, n in zip(origin, shape)]
262
  read = [[max(0, o - MARGIN), min(n, o + s + MARGIN)] for o, s, n in zip(origin, box_size, shape)]
263
  ch = meta["chunks"]
264
  nchunks = math.prod((b - 1) // c - a // c + 1 for (a, b), c in zip(read, ch))
265
  out = {"volume": v["volume"], "sample": v["sample"], "long_id": v["long_id"], "voxel_um": v["voxel_um"],
266
  "url": v["url"], "level": level, "level_shape_zyx": shape, "base_shape_zyx": base["shape"],
267
- "centre_level0_zyx": centre, "origin_zyx": origin, "size_zyx": box_size,
268
  "origin_level0_zyx": [o * f for o in origin], "size_level0_zyx": [s * f for s in box_size],
269
  "read_zyx": read, "chunks": nchunks, "chunk_bytes": math.prod(ch) * np.dtype(meta["dtype"]).itemsize,
270
  "model": model}
@@ -669,7 +669,7 @@ def _resize(img, shape):
669
 
670
 
671
  def picture(ct, arrays, boxes, axis=0):
672
- """One middle slice through the box along `axis` (0 z, 1 y, 2 x): CT, cos, grad_mag and the normal as colour
673
  (|x| red, |y| green, |z| blue), side by side at the CT's grid. Returns an RGB uint8 array."""
674
  def mid(a):
675
  return np.take(a, a.shape[axis] // 2, axis=axis)
@@ -691,7 +691,7 @@ def picture(ct, arrays, boxes, axis=0):
691
 
692
 
693
  PANELS = ("CT", "cos", "grad_mag", "normal")
694
- LEGEND = "normal as colour: x red, y green, z blue · villa lasagna, model output"
695
 
696
 
697
  def compose(pictures, title="", note=LEGEND):
@@ -799,7 +799,7 @@ def finish(p, prep, ran, work, out_root):
799
  "claim": "Model output: a lasagna field is geometry fitted by a model, not a reading of any text."}
800
  out_root = Path(out_root)
801
  out_root.mkdir(parents=True, exist_ok=True)
802
- stem = f"{p['sample']}-{p['volume']}-z{p['centre_level0_zyx'][0]}-y{p['centre_level0_zyx'][1]}-x{p['centre_level0_zyx'][2]}-s{p['size_zyx'][0]}"
803
  zpath = package(Path(ran["manifest"]).parent, p, arrays, receipt, out_root / f"{stem}.zip")
804
  return {"receipt": receipt, "zip": str(zpath), "pictures": pictures, "arrays": arrays}
805
 
 
252
  meta = zarray(v, level)
253
  shape = [int(s) for s in meta["shape"]]
254
  f = 2 ** level
255
+ center = [int(round(float(c))) for c in (z, y, x)]
256
+ for c, n in zip(center, base["shape"]):
257
  if not 0 <= c < n:
258
+ raise FieldError(f"the point {center} is outside the scan (level-0 shape {base['shape']})")
259
  origin = [max(0, min((c // f - size // 2 + ALIGN // 2) // ALIGN * ALIGN, (n - 1) // ALIGN * ALIGN))
260
+ for c, n in zip(center, shape)]
261
  box_size = [min(size, n - o) for o, n in zip(origin, shape)]
262
  read = [[max(0, o - MARGIN), min(n, o + s + MARGIN)] for o, s, n in zip(origin, box_size, shape)]
263
  ch = meta["chunks"]
264
  nchunks = math.prod((b - 1) // c - a // c + 1 for (a, b), c in zip(read, ch))
265
  out = {"volume": v["volume"], "sample": v["sample"], "long_id": v["long_id"], "voxel_um": v["voxel_um"],
266
  "url": v["url"], "level": level, "level_shape_zyx": shape, "base_shape_zyx": base["shape"],
267
+ "center_level0_zyx": center, "origin_zyx": origin, "size_zyx": box_size,
268
  "origin_level0_zyx": [o * f for o in origin], "size_level0_zyx": [s * f for s in box_size],
269
  "read_zyx": read, "chunks": nchunks, "chunk_bytes": math.prod(ch) * np.dtype(meta["dtype"]).itemsize,
270
  "model": model}
 
669
 
670
 
671
  def picture(ct, arrays, boxes, axis=0):
672
+ """One middle slice through the box along `axis` (0 z, 1 y, 2 x): CT, cos, grad_mag and the normal as color
673
  (|x| red, |y| green, |z| blue), side by side at the CT's grid. Returns an RGB uint8 array."""
674
  def mid(a):
675
  return np.take(a, a.shape[axis] // 2, axis=axis)
 
691
 
692
 
693
  PANELS = ("CT", "cos", "grad_mag", "normal")
694
+ LEGEND = "normal as color: x red, y green, z blue · villa lasagna, model output"
695
 
696
 
697
  def compose(pictures, title="", note=LEGEND):
 
799
  "claim": "Model output: a lasagna field is geometry fitted by a model, not a reading of any text."}
800
  out_root = Path(out_root)
801
  out_root.mkdir(parents=True, exist_ok=True)
802
+ stem = f"{p['sample']}-{p['volume']}-z{p['center_level0_zyx'][0]}-y{p['center_level0_zyx'][1]}-x{p['center_level0_zyx'][2]}-s{p['size_zyx'][0]}"
803
  zpath = package(Path(ran["manifest"]).parent, p, arrays, receipt, out_root / f"{stem}.zip")
804
  return {"receipt": receipt, "zip": str(zpath), "pictures": pictures, "arrays": arrays}
805
 
media/example-result.png CHANGED

Git LFS Details

  • SHA256: 2e05dabe3cb509d4403e671fcb5ea0f8e57f4766ed43c2afc39eb021415894a9
  • Pointer size: 131 Bytes
  • Size of remote file: 643 kB

Git LFS Details

  • SHA256: 3b809694026ae850c10c18905b738f34882c60358781e9d7a20531a3ff17594e
  • Pointer size: 131 Bytes
  • Size of remote file: 605 kB
media/howto-make.gif CHANGED

Git LFS Details

  • SHA256: f3cd25aaf4570fc66ff42a3266f17899c10bec804dee12d27e3c674057a55a8c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.71 MB

Git LFS Details

  • SHA256: 75279dfefa6d45065f65741ec8bdaf211f7bc063586ba233eea4cd3242a565bc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.8 MB
onescroll.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- space-onescroll:start -->
2
+ <!-- one page scroll bar: written by python -m kit spaceconnect render; do not edit -->
3
+ <style>iframe[srcdoc]{display:block;max-height:none!important}</style>
4
+ <script>
5
+ (function () {
6
+ if (window.__oneScroll) return;
7
+ window.__oneScroll = true;
8
+ var CSS = 'html,body{height:auto!important;min-height:0!important}html{overflow:hidden!important}';
9
+ function size(f) {
10
+ var d, st = f.__os;
11
+ try { d = f.contentDocument; } catch (e) { return; }
12
+ if (!d || !d.body || st.off) return;
13
+ var de = d.documentElement, r = Math.ceil(de.getBoundingClientRect().height), over = de.scrollHeight;
14
+ var ch = f.clientHeight, t = r;
15
+ if (over > ch + 1) t = Math.max(r, over);
16
+ else if (r >= st.r) t = Math.max(r, ch);
17
+ st.r = r;
18
+ if (Math.abs(t - ch) < 2) return;
19
+ var now = Date.now();
20
+ st.n = now - st.t0 < 4000 ? st.n + 1 : 1;
21
+ if (st.n === 1) st.t0 = now;
22
+ if (st.n > 40) { st.off = true; f.removeAttribute('scrolling'); var g = d.getElementById('space-onescroll'); if (g) g.remove(); return; }
23
+ f.style.setProperty('height', t + 'px', 'important');
24
+ }
25
+ function attach(f) {
26
+ var d, st = f.__os;
27
+ try { d = f.contentDocument; } catch (e) { return; }
28
+ if (!d || !d.documentElement || st.doc === d) return;
29
+ st.doc = d; st.r = Infinity; st.n = 0; st.off = false;
30
+ if (!d.getElementById('space-onescroll')) {
31
+ var s = d.createElement('style'); s.id = 'space-onescroll'; s.textContent = CSS;
32
+ (d.head || d.documentElement).appendChild(s);
33
+ }
34
+ f.setAttribute('scrolling', 'no');
35
+ f.style.setProperty('min-height', '0', 'important');
36
+ f.style.setProperty('max-height', 'none', 'important');
37
+ var w = d.defaultView, go = function () { size(f); };
38
+ if (w && w.ResizeObserver) { var ro = new w.ResizeObserver(go); ro.observe(d.documentElement); if (d.body) ro.observe(d.body); }
39
+ d.addEventListener('load', go, true);
40
+ go();
41
+ }
42
+ function bind(f) {
43
+ if (f.__os || f.getAttribute('data-scroll') === 'inner') return;
44
+ try { if (!f.contentDocument) return; } catch (e) { return; }
45
+ f.__os = {t0: 0, n: 0, r: 0};
46
+ f.addEventListener('load', function () { attach(f); });
47
+ if (f.contentDocument.readyState !== 'loading') attach(f);
48
+ else f.contentDocument.addEventListener('DOMContentLoaded', function () { attach(f); });
49
+ }
50
+ function scan() { var fs = document.getElementsByTagName('iframe'); for (var i = 0; i < fs.length; i++) bind(fs[i]); }
51
+ function start() {
52
+ scan();
53
+ new MutationObserver(scan).observe(document.documentElement, {childList: true, subtree: true});
54
+ window.addEventListener('resize', function () {
55
+ var fs = document.getElementsByTagName('iframe');
56
+ for (var i = 0; i < fs.length; i++) if (fs[i].__os) size(fs[i]);
57
+ });
58
+ }
59
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
60
+ })();
61
+ </script>
62
+ <!-- space-onescroll:end -->