web: package front into the wheel (splasher/web) + persist workspace layout
Browse files- README.md +4 -4
- splasher/server/app.py +2 -2
- splasher/web/src/app.js +41 -0
README.md
CHANGED
|
@@ -42,8 +42,8 @@ splasher/
|
|
| 42 |
Returns a *semantic* ViewState (points + per-point labels + channel,
|
| 43 |
BEV field, grid raster, selection, images) β not pixels.
|
| 44 |
server/ FastAPI backend on the same Session + serves the web front; desktop app.
|
| 45 |
-
web/
|
| 46 |
-
|
| 47 |
```
|
| 48 |
|
| 49 |
**One front, one engine.** The web front is served by the backend; the desktop app
|
|
@@ -55,7 +55,7 @@ exposes `view_state()` + commands (`paint_rect`, `select_rect`, `apply_selection
|
|
| 55 |
`set_frame`, `set_accum_radius`, `commit_grid`, `save`/`load`, β¦). Plugging in another front
|
| 56 |
= consuming this `Session` (the web does it over HTTP). **Colorization stays on the front
|
| 57 |
side**: each front draws the primitives its own way (reference helpers in
|
| 58 |
-
`splasher.engine.render`, mirrored in JS under `web/src`).
|
| 59 |
|
| 60 |
## Installation
|
| 61 |
|
|
@@ -84,7 +84,7 @@ Dark brutalist black-&-blue design. Labelable top-down (BEV) view (underlay mode
|
|
| 84 |
height / density / intensity) + **3D/camera panels you can add, resize, and each bind to a
|
| 85 |
channel** (3D color-by: height / intensity). Sensor placements (from `ChannelSpec.placement`)
|
| 86 |
are drawn as markers in 3D. Classes are fully editable (β in the *Classes* panel). Three.js
|
| 87 |
-
is hosted locally (`web/vendor`) β works offline.
|
| 88 |
|
| 89 |
> Native desktop window: the `app` extra ships a Qt WebEngine (Chromium) backend, so
|
| 90 |
> `uv sync --extra app && splasher demo` opens a real native window out of the box. If no
|
|
|
|
| 42 |
Returns a *semantic* ViewState (points + per-point labels + channel,
|
| 43 |
BEV field, grid raster, selection, images) β not pixels.
|
| 44 |
server/ FastAPI backend on the same Session + serves the web front; desktop app.
|
| 45 |
+
web/ web front (vanilla, zero build) β the ONLY front (packaged in the wheel).
|
| 46 |
+
vendor/ Three.js hosted locally (offline).
|
| 47 |
```
|
| 48 |
|
| 49 |
**One front, one engine.** The web front is served by the backend; the desktop app
|
|
|
|
| 55 |
`set_frame`, `set_accum_radius`, `commit_grid`, `save`/`load`, β¦). Plugging in another front
|
| 56 |
= consuming this `Session` (the web does it over HTTP). **Colorization stays on the front
|
| 57 |
side**: each front draws the primitives its own way (reference helpers in
|
| 58 |
+
`splasher.engine.render`, mirrored in JS under `splasher/web/src`).
|
| 59 |
|
| 60 |
## Installation
|
| 61 |
|
|
|
|
| 84 |
height / density / intensity) + **3D/camera panels you can add, resize, and each bind to a
|
| 85 |
channel** (3D color-by: height / intensity). Sensor placements (from `ChannelSpec.placement`)
|
| 86 |
are drawn as markers in 3D. Classes are fully editable (β in the *Classes* panel). Three.js
|
| 87 |
+
is hosted locally (`splasher/web/vendor`) β works offline.
|
| 88 |
|
| 89 |
> Native desktop window: the `app` extra ships a Qt WebEngine (Chromium) backend, so
|
| 90 |
> `uv sync --extra app && splasher demo` opens a real native window out of the box. If no
|
splasher/server/app.py
CHANGED
|
@@ -15,8 +15,8 @@ from ..engine.session import Session
|
|
| 15 |
from .files import combine_clouds, list_dir, open_file
|
| 16 |
from .protocol import encode_array, grid_from_dict, session_info_to_dict, view_state_to_dict
|
| 17 |
|
| 18 |
-
# Web front (vanilla, zero build) served as-is β `web/`
|
| 19 |
-
WEB_DIR = Path(__file__).resolve().parents[
|
| 20 |
|
| 21 |
|
| 22 |
def _as_session(session_or_source, labels) -> Session:
|
|
|
|
| 15 |
from .files import combine_clouds, list_dir, open_file
|
| 16 |
from .protocol import encode_array, grid_from_dict, session_info_to_dict, view_state_to_dict
|
| 17 |
|
| 18 |
+
# Web front (vanilla, zero build) served as-is β packaged at `splasher/web/`.
|
| 19 |
+
WEB_DIR = Path(__file__).resolve().parents[1] / "web"
|
| 20 |
|
| 21 |
|
| 22 |
def _as_session(session_or_source, labels) -> Session:
|
splasher/web/src/app.js
CHANGED
|
@@ -87,6 +87,9 @@ async function boot() {
|
|
| 87 |
apply(first);
|
| 88 |
fillGridForm(first.grid);
|
| 89 |
updateDims(readGridForm());
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
function apply(v) {
|
|
@@ -178,6 +181,44 @@ function updateSourceFromClouds() {
|
|
| 178 |
function onFilesChanged() {
|
| 179 |
if (fileMode) { buildClouds(); updateSourceFromClouds(); }
|
| 180 |
renderOpenViews();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
}
|
| 182 |
|
| 183 |
function buildAddBar() {
|
|
|
|
| 87 |
apply(first);
|
| 88 |
fillGridForm(first.grid);
|
| 89 |
updateDims(readGridForm());
|
| 90 |
+
|
| 91 |
+
await restoreWorkspace(); // reopen views + restore layout from last session
|
| 92 |
+
window.addEventListener("beforeunload", saveWorkspace);
|
| 93 |
}
|
| 94 |
|
| 95 |
function apply(v) {
|
|
|
|
| 181 |
function onFilesChanged() {
|
| 182 |
if (fileMode) { buildClouds(); updateSourceFromClouds(); }
|
| 183 |
renderOpenViews();
|
| 184 |
+
saveWorkspace();
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// ------------------------------------------------------------- workspace persistence
|
| 188 |
+
const WS_KEY = "splasher-workspace";
|
| 189 |
+
function saveWorkspace() {
|
| 190 |
+
if (!manager) return;
|
| 191 |
+
try {
|
| 192 |
+
const grow = (sel) => parseFloat(getComputedStyle(document.querySelector(sel)).flexGrow) || 1;
|
| 193 |
+
localStorage.setItem(WS_KEY, JSON.stringify({
|
| 194 |
+
files: manager.openFiles().map((f) => f.path),
|
| 195 |
+
dir: fsPath,
|
| 196 |
+
layout: {
|
| 197 |
+
rail: document.querySelector(".rail").getBoundingClientRect().width,
|
| 198 |
+
bev: grow(".bev-panel"), views: grow(".views"),
|
| 199 |
+
},
|
| 200 |
+
}));
|
| 201 |
+
} catch { /* ignore */ }
|
| 202 |
+
}
|
| 203 |
+
async function restoreWorkspace() {
|
| 204 |
+
let ws;
|
| 205 |
+
try { ws = JSON.parse(localStorage.getItem(WS_KEY) || "null"); } catch { ws = null; }
|
| 206 |
+
if (!ws) return;
|
| 207 |
+
if (ws.dir) fsPath = ws.dir;
|
| 208 |
+
if (ws.layout) {
|
| 209 |
+
const L = ws.layout;
|
| 210 |
+
if (L.rail) document.querySelector(".rail").style.flex = `0 0 ${L.rail}px`;
|
| 211 |
+
if (L.bev) document.querySelector(".bev-panel").style.flexGrow = L.bev;
|
| 212 |
+
if (L.views) document.querySelector(".views").style.flexGrow = L.views;
|
| 213 |
+
}
|
| 214 |
+
if (fileMode && ws.files && ws.files.length) {
|
| 215 |
+
const cb = manager.onFiles; manager.onFiles = null; // bulk: refresh once at the end
|
| 216 |
+
for (const path of ws.files) {
|
| 217 |
+
try { manager.addFile(await api.fsOpen(path)); } catch { /* file gone/changed */ }
|
| 218 |
+
}
|
| 219 |
+
manager.onFiles = cb;
|
| 220 |
+
onFilesChanged();
|
| 221 |
+
}
|
| 222 |
}
|
| 223 |
|
| 224 |
function buildAddBar() {
|