SmaugC137 commited on
Commit
2f6ea97
Β·
1 Parent(s): 2a86f9d

web: package front into the wheel (splasher/web) + persist workspace layout

Browse files
Files changed (3) hide show
  1. README.md +4 -4
  2. splasher/server/app.py +2 -2
  3. 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/ web front (vanilla, zero build) β€” the ONLY front.
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,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/` directory at the repo root.
19
- WEB_DIR = Path(__file__).resolve().parents[2] / "web"
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() {