/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
WIP: First draft at new interface with multiple comparison modes
Owen Melia <owenjmelia@gmail.com> committed commit 9389d7345972 parent a158c73 Browse files
2 changed files+146−33
index.htmlmodified+27−14View file
@@ -51,6 +51,12 @@
5151 }
5252 /* display:flex above would otherwise override the UA's [hidden] rule */
5353 .controls[hidden] { display: none; }
54+ .modes {
55+ display: flex; flex-wrap: wrap; gap: 8px;
56+ padding: 4px 0 10px; margin-bottom: 4px;
57+ border-bottom: 1px solid var(--line);
58+ }
59+ .modes .chip { font-size: 13px; padding: 4px 12px; }
5460 .controls label { color: var(--ink-2); font-size: 13px; white-space: nowrap; }
5561 select, input[type="number"], button {
5662 font: inherit; font-size: 13px;
@@ -116,10 +122,14 @@
116122 background: var(--sphere-bg); border-bottom: 1px solid var(--line);
117123 }
118124 .editor-head button { padding: 2px 10px; font-size: 12px; }
119- /* Source and compiled-op list side by side, so the editor gets the
120- height rather than sharing it with the list below. */
121- .editor-body { display: flex; align-items: stretch; }
122- .editor-code { position: relative; flex: 1 1 62%; min-width: 0; height: 34em; }
125+ /* Source and compiled-op list side by side. The fixed height lives on
126+ the row itself, not on either child: a child's own `height` would
127+ only be a *hint* stretch fills when unset, and `#compiled` sets its
128+ own smaller font, so the same em value would resolve to a shorter
129+ box than .editor-code's. Sizing the row instead makes both children
130+ stretch to one shared pixel height regardless of either's font. */
131+ .editor-body { display: flex; align-items: stretch; height: 34em; }
132+ .editor-code { position: relative; flex: 1 1 62%; min-width: 0; }
123133 /* The overlay and the textarea must agree on every metric that affects
124134 where a character lands. Keep these two rules together. */
125135 .editor-code > pre,
@@ -150,7 +160,9 @@
150160 color: var(--ink-2); white-space: pre;
151161 }
152162 @media (max-width: 860px) {
153- .editor-body { flex-direction: column; }
163+ /* Stacked, so each panel goes back to managing its own height rather
164+ than sharing the row's fixed one. */
165+ .editor-body { flex-direction: column; height: auto; }
154166 .editor-code { flex: none; height: 26em; }
155167 #compiled { border-left: 0; border-top: 1px solid var(--line); max-height: 12em; }
156168 }
@@ -229,6 +241,16 @@
229241 browser by <a href="https://numbl.org">numbl</a>. Edit either and watch
230242 it change. Drag to rotate.
231243 </p>
244+ <div class="modes" id="modebar">
245+ <button type="button" class="chip" id="mode-simulate" aria-pressed="true">Simulate</button>
246+ <button type="button" class="chip" id="mode-effort"
247+ title="Run several solver settings side by side on one clock">Compare computational effort</button>
248+ <button type="button" class="chip" id="mode-vs-sphere" disabled title="Coming soon">Compare against sphere</button>
249+ <button type="button" class="chip" id="mode-vs-upload"
250+ title="Check this solver against a saved reference run.">
251+ Compare against uploaded data</button>
252+ <input type="file" id="cmp-file" accept=".h5" hidden>
253+ </div>
232254 <div class="controls ctrl-group" data-group="surface">
233255 <label>preset
234256 <select id="model"></select>
@@ -241,15 +263,6 @@
241263 <div class="controls" id="params"></div>
242264 <div class="controls" id="geomparams"></div>
243265 </div>
244- <!-- Transitional: the mode-entry buttons move into a top-level `.modes`
245- row in the next phase. Structure/behavior unchanged for now. -->
246- <div class="controls">
247- <button id="comparetoggle" title="Run several solver settings side by side on one clock">Compare</button>
248- <button id="cmp-load"
249- title="Check this solver against a saved reference run (.h5, the layout in docs/ellipsoid-reference-spec.md): opens a comparison seeded from its exact initial state, with its own settings, that runs to its end time and is measured against its final state.">
250- Compare to reference…</button>
251- <input type="file" id="cmp-file" accept=".h5" hidden>
252- </div>
253266 <div class="controls" id="comparebar" hidden>
254267 <div class="cmp-axes">
255268 <div class="cmp-axis">
src/main.tsmodified+119−19View file
@@ -63,13 +63,14 @@ const elMovieSpeed = $<HTMLSelectElement>('moviespeed');
6363 const elMovieRes = $<HTMLSelectElement>('movieres');
6464 const elMovieRotate = $<HTMLInputElement>('movierotate');
6565 const elMovie = $<HTMLButtonElement>('movie');
66-const elCompareToggle = $<HTMLButtonElement>('comparetoggle');
66+const elModeSimulate = $<HTMLButtonElement>('mode-simulate');
67+const elModeEffort = $<HTMLButtonElement>('mode-effort');
68+const elModeVsUpload = $<HTMLButtonElement>('mode-vs-upload');
6769 const elCompareBar = $('comparebar');
6870 const elCmpNiter = $('cmp-niter');
6971 const elCmpLmax = $('cmp-lmax');
7072 const elCmpDt = $('cmp-dt');
7173 const elCmpRef = $<HTMLSelectElement>('cmp-ref');
72-const elCmpLoad = $<HTMLButtonElement>('cmp-load');
7374 const elCmpFile = $<HTMLInputElement>('cmp-file');
7475 const elCmpFileInfo = $('cmp-fileinfo');
7576 const elCmpFileClear = $<HTMLButtonElement>('cmp-fileclear');
@@ -93,6 +94,21 @@ const elEditorFile = $<HTMLSelectElement>('editor-file');
9394 const elRecompile = $<HTMLButtonElement>('recompile');
9495 const elRevert = $<HTMLButtonElement>('revert');
9596
97+/** The named groups the control area is organized into (index.html's
98+ * `.ctrl-group[data-group]` wrappers). Each mode shows a declared subset of
99+ * these — see MODE_GROUPS and applyModeVisibility below. */
100+const GROUP_NAMES = [
101+ 'surface', 'surface-params', 'solver', 'display',
102+ 'playback', 'benchmark', 'seed', 'movie',
103+] as const;
104+type GroupName = (typeof GROUP_NAMES)[number];
105+const groupEls: Record<GroupName, HTMLElement> = Object.fromEntries(
106+ GROUP_NAMES.map((name) => [
107+ name,
108+ document.querySelector(`.ctrl-group[data-group="${name}"]`) as HTMLElement,
109+ ]),
110+) as Record<GroupName, HTMLElement>;
111+
96112 for (const p of presets) {
97113 const o = document.createElement('option');
98114 o.value = p.key;
@@ -1349,7 +1365,87 @@ function applyRefUi(): void {
13491365 refreshVariants();
13501366 }
13511367
1352-elCmpLoad.addEventListener('click', () => elCmpFile.click());
1368+/**
1369+ * The four top-level modes and which control groups each shows (see
1370+ * GROUP_NAMES/groupEls above; `.ctrl-group` wrappers in index.html).
1371+ * `currentMode` tracks which configuration is on screen — the compare bar
1372+ * being open, and in which flavor — not whether a study has actually been
1373+ * started inside it. That match matters: without it, opening the bar
1374+ * (which already shows the right groups) leaves its top-row button
1375+ * unhighlighted until a study happens to start, which is inconsistent with
1376+ * `vs-upload`'s one-click flow and reads as broken.
1377+ */
1378+type Mode = 'simulate' | 'compute-effort' | 'vs-sphere' | 'vs-upload';
1379+let currentMode: Mode = 'simulate';
1380+
1381+const MODE_GROUPS: Record<Mode, readonly GroupName[]> = {
1382+ simulate: ['surface', 'surface-params', 'solver', 'display', 'playback', 'benchmark', 'seed', 'movie'],
1383+ 'compute-effort': ['surface', 'surface-params', 'display', 'playback', 'seed'],
1384+ 'vs-sphere': [], // unreachable — the button is disabled, no listener ever calls setMode with this
1385+ 'vs-upload': ['display', 'playback', 'seed'],
1386+};
1387+
1388+function setModeButtons(mode: Mode): void {
1389+ elModeSimulate.setAttribute('aria-pressed', String(mode === 'simulate'));
1390+ elModeEffort.setAttribute('aria-pressed', String(mode === 'compute-effort'));
1391+ elModeVsUpload.setAttribute('aria-pressed', String(mode === 'vs-upload'));
1392+}
1393+
1394+/** Show exactly the groups `mode` declares; hide the rest. */
1395+function applyModeVisibility(mode: Mode): void {
1396+ currentMode = mode;
1397+ const shown = new Set<GroupName>(MODE_GROUPS[mode]);
1398+ for (const name of GROUP_NAMES) groupEls[name].hidden = !shown.has(name);
1399+ setModeButtons(mode);
1400+}
1401+
1402+/**
1403+ * Enter `mode`: groups, top-row buttons, and the compare bar's own
1404+ * visibility (open for the two compare flavors, closed for Simulate).
1405+ * Doesn't touch `compareRun`/`refCase` or start/stop a study — callers
1406+ * decide that; this only decides what's on screen, and it decides it
1407+ * immediately, so the button you clicked lights up right away rather than
1408+ * waiting on a study that may not exist yet (or may never start, if the
1409+ * bar's own Compare is never pressed).
1410+ */
1411+function enterMode(mode: Mode): void {
1412+ applyModeVisibility(mode);
1413+ elCompareBar.hidden = mode === 'simulate';
1414+}
1415+
1416+/** Entering a mode from the top row. */
1417+function setMode(mode: Mode): void {
1418+ if (mode === 'vs-sphere') return; // unreachable — button is disabled
1419+ if (mode === 'simulate') {
1420+ if (compareRun) void stopCompare();
1421+ enterMode('simulate');
1422+ return;
1423+ }
1424+ if (mode === 'compute-effort') {
1425+ if (compareRun && refCase) {
1426+ // A vs-upload study is actively running; leave it running, in its own
1427+ // mode. Stop it first (Simulate, or the bar's own Stop) to switch.
1428+ return;
1429+ }
1430+ // Only safe to drop a loaded file while nothing is using it — the same
1431+ // guard elCmpFileClear itself observes (disabled while a study runs).
1432+ if (refCase) {
1433+ refCase = null;
1434+ applyRefUi();
1435+ }
1436+ enterMode('compute-effort');
1437+ return;
1438+ }
1439+ // vs-upload: opens the file picker; entering the mode itself happens once
1440+ // a file is actually chosen (elCmpFile's change handler below) — not here,
1441+ // since cancelling the dialog must leave the current mode untouched.
1442+ elCmpFile.click();
1443+}
1444+
1445+elModeSimulate.addEventListener('click', () => setMode('simulate'));
1446+elModeEffort.addEventListener('click', () => setMode('compute-effort'));
1447+elModeVsUpload.addEventListener('click', () => setMode('vs-upload'));
1448+
13531449 elCmpFile.addEventListener('change', () => {
13541450 const file = elCmpFile.files?.[0];
13551451 // Cleared so picking the same file again still fires a change event.
@@ -1377,7 +1473,7 @@ elCmpFile.addEventListener('change', () => {
13771473 cmpSelected.dt.clear();
13781474 cmpSelected.dt.add(1);
13791475 applyRefUi();
1380- elCompareBar.hidden = false;
1476+ enterMode('vs-upload');
13811477 if (compareRun) {
13821478 // A study is already up (this one loaded over it): same teardown as
13831479 // rebuildCompare, then the new file's study takes its place.
@@ -1391,10 +1487,9 @@ elCmpFile.addEventListener('change', () => {
13911487 elCmpFileClear.addEventListener('click', () => {
13921488 refCase = null;
13931489 applyRefUi();
1394-});
1395-
1396-elCompareToggle.addEventListener('click', () => {
1397- elCompareBar.hidden = !elCompareBar.hidden;
1490+ // The bar stays open — this only drops back to the plain chip comparison.
1491+ // Only reachable while idle (elCmpFileClear is disabled during a study).
1492+ enterMode('compute-effort');
13981493 });
13991494
14001495 elCmpStart.addEventListener('click', () => {
@@ -1402,22 +1497,26 @@ elCmpStart.addEventListener('click', () => {
14021497 else void startCompare();
14031498 });
14041499
1405-/** Controls the study supersedes or cannot honour while it is running. */
1500+/**
1501+ * Controls the study supersedes or cannot honour while it is running.
1502+ * Mode/group/button state is not this function's job — that's set the
1503+ * moment a mode is entered (enterMode, above), independent of whether a
1504+ * study inside it has actually started or stopped.
1505+ */
14061506 function setCompareUi(on: boolean): void {
1407- for (const el of [
1408- elNiter, elLmax, elOversample, elBenchmark, elMovieToggle,
1409- // Clearing the file out from under a running study would leave it
1410- // checking against a file that is no longer loaded. Loading stays
1411- // enabled: a new file tears the study down and opens its own.
1412- elCmpFileClear,
1413- ]) {
1414- el.disabled = on;
1415- }
1507+ // A study picks its own display grid, so oversample stays individually
1508+ // disabled inside the still-visible display group; and clearing a loaded
1509+ // file out from under a running study would leave it checking against one
1510+ // that no longer exists.
1511+ elOversample.disabled = on;
1512+ elCmpFileClear.disabled = on;
14161513 elCmpNiter.querySelectorAll('button').forEach((b) => (b.disabled = on));
14171514 elCmpLmax.querySelectorAll('button').forEach((b) => (b.disabled = on));
14181515 elCmpDt.querySelectorAll('button').forEach((b) => (b.disabled = on));
14191516 elCmpStart.textContent = on ? 'Stop comparing' : 'Compare';
1420- elCompareToggle.textContent = on ? 'Comparing' : 'Compare';
1517+ // The movie bar's own hidden flag is independent of the movie *group's* —
1518+ // force it closed so it doesn't reappear open once the group is shown
1519+ // again on returning to Simulate.
14211520 if (on) elMovieBar.hidden = true;
14221521 }
14231522
@@ -1503,6 +1602,7 @@ async function rebuildCompare(): Promise<void> {
15031602
15041603 // ---------------------------------------------------------------- boot
15051604 async function boot(): Promise<void> {
1605+ enterMode('simulate');
15061606 elModel.value = presets[0].key;
15071607 // The iteration count is one default shared with the benchmark, like the
15081608 // rest of the RunSpec's — take it from there rather than from the markup, so
moveopenescclose