|
| 1 | +# Design |
| 2 | + |
| 3 | +The visual and interaction record for OpenOptiSim's interface. It exists because |
| 4 | +[`docs/ui-mockup.html`](docs/ui-mockup.html) is a *build*, and a build without a |
| 5 | +written rationale is a set of numbers nobody can argue with later. Everything |
| 6 | +here was decided against the audience and use scene in [PRODUCT.md](PRODUCT.md); |
| 7 | +where a decision was made by measurement rather than judgement, the measurement |
| 8 | +is given. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. What the surface is |
| 13 | + |
| 14 | +An **Operate** surface: a schematic editor, used to complete a task rather than |
| 15 | +to be admired or read through. Scanability, consistency and the real usage scene |
| 16 | +outrank expression. Brand lives in precise details, not in gestures. |
| 17 | + |
| 18 | +The engineer is *iterating* — change a parameter, run, look at the constellation, |
| 19 | +change it again — dozens of times an hour. Three consequences follow, and they |
| 20 | +decide most of what is below: |
| 21 | + |
| 22 | +1. **Density beats generosity.** Screen space spent on decoration is space not |
| 23 | + spent on the schematic or the plot. |
| 24 | +2. **Recognition beats novelty.** The layout is the one this audience already |
| 25 | + knows from Blender, VS Code and EDA tools. Nothing is moved to be different. |
| 26 | +3. **Nothing on screen may be invented.** Every number, every parameter, every |
| 27 | + port comes from a real engine run. See §7. |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 2. Two grounds |
| 32 | + |
| 33 | +The interface ships **paper** and **graphite**, and defaults to paper. |
| 34 | + |
| 35 | +A schematic is a document before it is a screen. Its plots leave the tool for |
| 36 | +reports, theses and papers, where a screenshot off a black canvas is the wrong |
| 37 | +artefact. Graphite is one click away for the night session beside a terminal. |
| 38 | + |
| 39 | +Neither is a skin over the other: |
| 40 | + |
| 41 | +| | paper | graphite | |
| 42 | +| :--- | :--- | :--- | |
| 43 | +| ground | `#e9edf3` bed under white panels | `#0b0e13` under `#11151c` panels | |
| 44 | +| a block | white card on a grey bed | raised panel on a dark one | |
| 45 | +| hover | adds **ink** (darker) | adds **light** | |
| 46 | +| plot bed | `#ffffff` — a plot is on paper | `#080b10` | |
| 47 | + |
| 48 | +The port hues are the identity and survive both, but their **values are |
| 49 | +re-derived per ground rather than inverted** — on paper the spectrum darkens to |
| 50 | +hold contrast against a light bed. |
| 51 | + |
| 52 | +**Structure.** The bare `:root` block carries the complete paper palette. The |
| 53 | +graphite tokens appear twice: once under |
| 54 | +`@media (prefers-color-scheme: dark)` guarded as `:root:not([data-theme="light"])`, |
| 55 | +and once under `:root[data-theme="dark"]` so an explicit choice wins in both |
| 56 | +directions. No colour is ever defined *only* inside a media or `[data-theme]` |
| 57 | +block — that is the classic unreadable-page bug, and it is checked rather than |
| 58 | +assumed (§8). |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## 3. The spectral port identity |
| 63 | + |
| 64 | +**Every port colour is a wavelength, not a preference.** A wire's colour tells |
| 65 | +you what travels down it. |
| 66 | + |
| 67 | +| port type | paper | graphite | |
| 68 | +| :--- | :--- | :--- | |
| 69 | +| optical (C-band) | `#0a8279` | `#22d3c5` | |
| 70 | +| electrical | `#a35f00` | `#f0a030` | |
| 71 | +| binary | `#5a6474` | `#7c86a0` | |
| 72 | +| symbol | `#6539cc` | `#9b7cf6` | |
| 73 | +| metric | `#bc2668` | `#e85d9b` | |
| 74 | + |
| 75 | +This is the one place the interface is allowed to be memorable, and it earns its |
| 76 | +place by being *information*: a typed-port system that refuses invalid wiring at |
| 77 | +edit time is meaningless if the types are invisible. |
| 78 | + |
| 79 | +The shown project is chosen so that **all five types appear on the canvas at |
| 80 | +once**. That is why it is a coherent link rather than an on-off-keyed one: only |
| 81 | +a coherent chain carries binary into symbols, symbols into two electrical |
| 82 | +drives, an optical field, two photocurrents back, and symbols out again. |
| 83 | + |
| 84 | +Accent is `--optical`, because the tool is an optical simulator. Semantic colour |
| 85 | +(`--good` / `--warn` / `--bad`) is kept separate from it and does not count as |
| 86 | +the accent. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## 4. The ink ladder |
| 91 | + |
| 92 | +Four layers, spaced so hierarchy comes from the **distance between them** rather |
| 93 | +than from pushing the weakest below readability. |
| 94 | + |
| 95 | +| token | paper | vs white | graphite | role | |
| 96 | +| :--- | :--- | ---: | :--- | :--- | |
| 97 | +| `--ink` | `#0f141c` | 18.5 | `#e6ecf5` | values, headings | |
| 98 | +| `--ink-2` | `#333d4b` | 11.0 | `#a8b4c6` | labels, body | |
| 99 | +| `--ink-3` | `#4c5666` | 7.4 | `#8e9bb0` | section titles, axes | |
| 100 | +| `--ink-4` | `#5f6a79` | 5.5 | `#7a869b` | units, ranges, disabled | |
| 101 | + |
| 102 | +The bottom rung is set by the **tinted grounds it actually lands on** — a unit |
| 103 | +inside an input well, a disabled toolbar label — not by white, where it would |
| 104 | +look too pale. Against the darkest surface it still clears 4.5:1. |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## 5. Layout |
| 109 | + |
| 110 | +Four regions, top to bottom: |
| 111 | + |
| 112 | +``` |
| 113 | +menu bar 34px File / Edit / Simulate / View / Help, ground switch, engine state |
| 114 | +action toolbar 40px Run, Stop, Sweep · select/pan/align · zoom · run context |
| 115 | +body 1fr palette 216px | node canvas 1fr | inspector 260px |
| 116 | +results dock 292px tabs + metric strip, plot + side readouts |
| 117 | +status bar 24px block and link counts, selection, sample count, precision |
| 118 | +``` |
| 119 | + |
| 120 | +**Run sits leftmost in the toolbar**, where the eye lands first, because it is |
| 121 | +the action taken dozens of times an hour. |
| 122 | + |
| 123 | +**The dock is 292px** because the constellation's plot is square and therefore |
| 124 | +sized by the dock's *height*, not its width. A shorter dock wastes the width it |
| 125 | +has. |
| 126 | + |
| 127 | +Below 940px the palette and inspector collapse and a note says so. This is a |
| 128 | +desktop tool; the real build would dock them as overlays rather than dropping |
| 129 | +them, and the note says that too rather than pretending the narrow layout is the |
| 130 | +intended one. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## 6. Plots |
| 135 | + |
| 136 | +Two reduced result types, and they share a visual language because they mean the |
| 137 | +same thing: **distance from the page is how many landed there**. |
| 138 | + |
| 139 | +- **Eye diagram** — density ramp bed → cyan → far stop. |
| 140 | +- **Constellation** — the same ramp; axes through the origin, because a |
| 141 | + constellation is read relative to zero and not to the corner of a box; ideal |
| 142 | + points marked with **crosses rather than filled dots**, so a cluster's own |
| 143 | + centre stays visible underneath and a bias offset reads instead of being |
| 144 | + covered by the marker meant to locate it. |
| 145 | +- **Sensitivity** — one curve per format, each labelled where it crosses the FEC |
| 146 | + threshold rather than in a legend box. The threshold is 1e-3, not Q = 6: |
| 147 | + nobody operates an error-free channel any more, they operate one a |
| 148 | + soft-decision code can close. |
| 149 | + |
| 150 | +The ramp's mid stop is placed by **luminance**, at roughly the same fraction of |
| 151 | +each ground's span, which is what keeps sparse outliers subordinate to the dense |
| 152 | +regions on both. |
| 153 | + |
| 154 | +A canvas has no cascade, so the plots read the same tokens as everything else at |
| 155 | +draw time and are redrawn when the ground changes. Nothing below the token block |
| 156 | +names a colour. |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## 7. Real data, always |
| 161 | + |
| 162 | +The palette is generated from `manifests()` — the same call the real GUI will |
| 163 | +make. The inspector shows true parameters, units, ranges and docstrings. The |
| 164 | +constellation, eye and sweep are a real run, exported by |
| 165 | +[`examples/export_ui_data.py`](examples/export_ui_data.py). |
| 166 | + |
| 167 | +This is not a purity exercise. Building the mockup against real data has twice |
| 168 | +found engine defects that the test suite missed: |
| 169 | + |
| 170 | +- `eye_histogram` accepted any `time_bins` and rendered a 32-sample trace across |
| 171 | + 96 columns as **vertical banding**. Time resolution is now capped at one |
| 172 | + column per sample. |
| 173 | +- Placing an analyser after the differential decoder reported **EVM of exactly |
| 174 | + zero** however bad the link was, because that block emits decisions. The |
| 175 | + mockup now carries two analysers — soft measurement before the decoder, error |
| 176 | + count after — which is also how a bench does it. |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## 8. What is verified, and how |
| 181 | + |
| 182 | +Not by looking at downscaled screenshots. Each publish is checked by |
| 183 | +measurement, in **both grounds**: |
| 184 | + |
| 185 | +- **Contrast** — every text node's computed colour against its resolved |
| 186 | + background; ~110 elements per dock tab. Target 4.5:1 (3:1 for large text). |
| 187 | + Current status: **zero failures**, both grounds, all four tabs. |
| 188 | +- **Token completeness** — every `var(--…)` the stylesheet references must |
| 189 | + resolve from bare `:root` alone. Current: 34/34. |
| 190 | +- **Theme-block drift** — the two graphite blocks are compared key by key. |
| 191 | +- **Canvas repaint** — plot beds are sampled after switching, in both |
| 192 | + directions. |
| 193 | +- **Geometry** — no SVG element may stray outside the viewBox; no horizontal |
| 194 | + page overflow at 1440px. |
| 195 | + |
| 196 | +Two findings worth keeping: |
| 197 | + |
| 198 | +- A contrast audit run while the browser pane was not compositing reported a |
| 199 | + clean result that **meant nothing** — the page had laid out at its narrow |
| 200 | + breakpoint, where the palette and inspector are `display: none` and were never |
| 201 | + checked. Verify the viewport before trusting the audit. |
| 202 | +- The design detector once ran degraded and returned `[]`. An empty result from |
| 203 | + a degraded tool is an undercount, not a pass. |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## 9. Decisions worth not re-litigating |
| 208 | + |
| 209 | +- **Paper is the default**, and it is stamped before first paint so a dark host |
| 210 | + never flashes through. The stylesheet still answers `prefers-color-scheme` on |
| 211 | + its own for the no-script case. |
| 212 | +- **A backward wire is the schematic wrapping**, not a mistake. It needs a |
| 213 | + *tighter* bezier control offset than a forward one; scaling the offset with |
| 214 | + the span throws the curve outside the canvas on exactly the wire that already |
| 215 | + travels furthest. |
| 216 | +- **Node labels truncate with the full name in a tooltip.** A fixed-width box |
| 217 | + that lets long names spill across neighbours is worse than one that clips. |
| 218 | +- **Hover on paper adds ink, not light.** Lightening a hover on a white ground |
| 219 | + moves it towards invisible. |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +## 10. Not done |
| 224 | + |
| 225 | +- The mockup shows a single-carrier link. The dual-polarization link is in |
| 226 | + [`examples/dualpol_link.py`](examples/dualpol_link.py) and is not on the |
| 227 | + canvas: at ~20 blocks the node text stops being readable at this canvas size, |
| 228 | + and a schematic nobody can read is not a better demonstration. |
| 229 | +- No motion beyond the run pulse and the control transitions. |
| 230 | +- No empty, loading or error states — there is no session server yet to produce |
| 231 | + them. |
0 commit comments