"""3D Scroll: the fit's mesh as a compact point cloud, so the page can roll and unroll the scroll in the browser. GET /api/mesh3d.bin?stride=N the binary below; N is 4, 8 or 16 (default 8) on the in-memory grid GET /api/mesh3d.json?stride=N the same header as JSON, with the byte layout, for tools The in-memory mesh keeps every XYZ_STRIDE-th node (2) of the 956 by 56,913 tifxyz grid; stride N keeps every N-th of those, so a node is every 2N-th grid step (0.384 mm times N along the sheet). Only nodes whose x, y and z are all valid are sent, in row-major order over the sampled grid. Built once per stride with numpy and kept in memory. Layout of mesh3d.bin, little-endian throughout (HEADER_BYTES = 72, then four arrays, 72 + 13 n bytes in all): offset type field 0 char[4] magic "P4S3" 4 uint16 version (1) 6 uint16 stride N 8 uint32 n, the number of nodes 12 uint32 rows of the sampled grid (before invalid nodes are dropped) 16 uint32 cols of the sampled grid 20 uint32 map width in px (142,283) 24 uint32 map height in px (2,390) 28 float32[3] bbox min x, y, z of the nodes, in 9.6 um scan voxels 40 float32[3] bbox max x, y, z 52 float32 map px per flat unit (2.5: one full mesh grid step) 56 float32 mm per map px (0.0768) 60 float32 mm per voxel (0.0096) 64 uint16 ink pyramid level k 66 uint16 ink window side in map px (2 ** (k + 1)) 68 uint32 pad, 0 72 uint16[n][3] position x, y, z: voxel = min + q * (max - min) / 65535, per axis 72+6n uint16[n][2] flat position: full mesh grid column and row; map x = 2.5 col, map y = 2.5 row 72+10n int16[n] winding at the node (-1 no winding, -2 ambiguous) 72+12n uint8[n] ink: mean of the map pixels in a 2 ** (k + 1) px square centered on the node, from pyramid level k, the level whose 2x2 window is at most the node spacing; 0 where the map has no ink data (exactly 0 there, which the page draws as fine dots, not as low ink) Scan axes: x and y across the scroll, z along it (up is high z, the top of the map). Map x runs from the outer end (x = 0) to the inner end. An ink map is model output, not a reading. """ from __future__ import annotations import math import struct import threading import time import numpy as np from fastapi import APIRouter, HTTPException, Query from fastapi.responses import JSONResponse, Response class _Core: """app.py, looked up on use: app imports this module at its end, so a top-level import would be circular.""" def __getattr__(self, name): import app return getattr(app, name) core = _Core() router = APIRouter() MAGIC = b"P4S3" VERSION = 1 STRIDES = (4, 8, 16) VOXEL_MM = 9.6e-3 HEADER = struct.Struct("<4sHHIIIII3f3ffffHHI") HEADER_BYTES = HEADER.size # 72 HEADER_FIELDS = [ # name, struct code, offset: the same layout as the docstring, for tools that read the JSON ("magic", "4s", 0), ("version", "uint16", 4), ("stride", "uint16", 6), ("n", "uint32", 8), ("rows", "uint32", 12), ("cols", "uint32", 16), ("map_width", "uint32", 20), ("map_height", "uint32", 24), ("bbox_min_voxels", "float32[3]", 28), ("bbox_max_voxels", "float32[3]", 40), ("map_px_per_flat_unit", "float32", 52), ("mm_per_px", "float32", 56), ("mm_per_voxel", "float32", 60), ("ink_level", "uint16", 64), ("ink_window_px", "uint16", 66), ("pad", "uint32", 68)] CACHE = {"Cache-Control": "public, max-age=86400"} _cache: dict = {} # stride -> (source key, bytes, header dict) _build_lock = threading.Lock() def _source_key(): D = core.D return id(D["X"]), D["X"].shape, id(D["ink"]) def build(stride: int = 8): """(bytes, header) for one stride; numpy only, a fraction of a second for the full Paris 4 grid.""" t0 = time.perf_counter() D = core.D xyz_stride, per_grid = core.XYZ_STRIDE, core.INK_PER_GRID X, Y, Z = (D[k][::stride, ::stride] for k in "XYZ") rows, cols = X.shape ok = np.isfinite(X) & np.isfinite(Y) & np.isfinite(Z) ii, jj = np.nonzero(ok) # row-major order n = int(ii.size) P = np.stack([X[ok], Y[ok], Z[ok]], 1).astype(np.float32) if n: lo, hi = P.min(0), P.max(0) else: lo = hi = np.zeros(3, np.float32) rng = (hi - lo).astype(np.float32) q = np.zeros((n, 3), np.uint16) for a in range(3): if rng[a] > 0: q[:, a] = np.round((P[:, a] - lo[a]) / rng[a] * 65535.0).clip(0, 65535).astype(np.uint16) # full mesh grid position of each node, which is also its flat position gr = ii.astype(np.int64) * stride * xyz_stride gc = jj.astype(np.int64) * stride * xyz_stride if n and (gr.max() > 65535 or gc.max() > 65535): raise HTTPException(500, "the mesh grid is too large for uint16 flat positions") flat = np.stack([gc, gr], 1).astype(np.uint16) g = D["wind"] wind = g[np.minimum(gr, g.shape[0] - 1), np.minimum(gc, g.shape[1] - 1)].astype(np.int16) # ink: a 2x2 window at pyramid level k centered on the node, no wider than the node spacing spacing = stride * xyz_stride * per_grid pyr = D["pyr"] k = int(min(max(0, math.floor(math.log2(spacing)) - 1), len(pyr) - 1)) a = pyr[k] h, w = a.shape s = float(2 ** k) mx, my = gc * per_grid, gr * per_grid x0 = np.clip(np.floor(mx / s - 0.5).astype(np.int64), 0, max(w - 2, 0)) y0 = np.clip(np.floor(my / s - 0.5).astype(np.int64), 0, max(h - 2, 0)) x1, y1 = np.minimum(x0 + 1, w - 1), np.minimum(y0 + 1, h - 1) tot = (a[y0, x0].astype(np.uint16) + a[y0, x1] + a[y1, x0] + a[y1, x1]) ink = ((tot + 2) // 4).astype(np.uint8) W, H = int(D["W"]), int(D["H"]) head = HEADER.pack(MAGIC, VERSION, stride, n, rows, cols, W, H, *map(float, lo), *map(float, hi), float(per_grid), float(core.MM_PER_PX), VOXEL_MM, k, 2 ** (k + 1), 0) body = b"".join([head, q.astype("