Skip to content

Commit 9f248f7

Browse files
ehsun-shclaude
andcommitted
Show the interface in the README, and make the canvas fit its region
A GUI project whose front page has no picture of the GUI is hiding the work. docs/images/ now carries both grounds and the README leads with them, under a caption that says plainly this is a build of the planned interface and not a running application — the status banner two paragraphs below says "no GUI yet" and the two must not appear to contradict each other. Taking the screenshot is what exposed a wrong claim I had committed earlier. DESIGN.md said widening the viewBox to 1000x420 moved its aspect "closer to the 2.29 the canvas region actually has". Measured, the region is 964x510 — an aspect of 1.89. So the change moved away from it and doubled the letterboxing, from 50px wasted to 105px. The bands were visible in the capture. Fixed at the cause rather than in the sentence: the viewBox is 1000x530, which matches 1.89 and wastes nothing, and the three rows spread into the height they gained. DESIGN.md now carries the measurement instead of the estimate. docs/capture_screenshots.py makes the refresh one command, since it has to happen on every mockup change to be worth anything. It refuses to run if the theme bootstrap it patches has moved, because the silent failure is two paper captures with one of them labelled graphite. The contrast audit gained a viewport assertion and now reports how many nodes it inspected. This is the second time an audit has run at the wrong size — a fresh tab does not inherit the previous one's emulated viewport — and a zero from an audit that inspected nothing looks exactly like a pass. Verified at 1440px: region 1.89 against viewBox 1.89, zero wasted pixels, no stray geometry, legend clear of every block, no horizontal overflow, and zero contrast failures across 111-132 nodes per dock tab in both grounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent a97d45a commit 9f248f7

6 files changed

Lines changed: 180 additions & 27 deletions

File tree

DESIGN.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,19 @@ the action taken dozens of times an hour.
126126
sized by the dock's *height*, not its width. A shorter dock wastes the width it
127127
has.
128128

129-
**The canvas viewBox is 1000 x 420**, seven columns of 138px at a node width of
130-
116. The seventh column arrived with the span and its compensator; the
131-
alternative was to keep six and squeeze the spacing to 120, which leaves four
132-
pixels between adjacent blocks and reads as cramped. Widening also moved the
133-
aspect ratio to 2.38, closer to the 2.29 the canvas region actually has at
134-
1440px than the 2.10 it had before.
129+
**The canvas viewBox is 1000 x 530**, seven columns of 138px at a node width of
130+
116, three rows at y = 30 / 220 / 410. The seventh column arrived with the span
131+
and its compensator; the alternative was to keep six and squeeze the spacing to
132+
120, which leaves four pixels between adjacent blocks and reads as cramped.
133+
134+
The *height* is set by the region the SVG actually lands in, and that number is
135+
worth measuring rather than estimating. At 1440px the canvas region is
136+
**964 x 510 — an aspect of 1.89**. With `preserveAspectRatio="xMidYMid meet"`
137+
any box wider than that is letterboxed top and bottom: the original 880 x 420
138+
(2.10) wasted 50px, and widening to 1000 x 420 (2.38) wasted **105px** — it made
139+
the fit worse, not better, which is the opposite of what a first pass here
140+
claimed. Matching the box to 1.89 wastes none, and the rows spread into the
141+
height they gained.
135142

136143
Below 940px the palette and inspector collapse and a note says so. This is a
137144
desktop tool; the real build would dock them as overlays rather than dropping
@@ -203,15 +210,25 @@ measurement, in **both grounds**:
203210
- **Theme-block drift** — the two graphite blocks are compared key by key.
204211
- **Canvas repaint** — plot beds are sampled after switching, in both
205212
directions.
206-
- **Geometry** — no SVG element may stray outside the viewBox; no horizontal
207-
page overflow at 1440px.
213+
- **Geometry** — no SVG element may stray outside the viewBox; the viewBox
214+
aspect must match the canvas region's, or the difference is letterboxed away;
215+
the Ports legend must not overlap a block; no horizontal page overflow.
216+
- **Screenshots**`python docs/capture_screenshots.py` regenerates
217+
[`docs/images/`](docs/images/) in both grounds at the same 1440px the audits
218+
use, so what the README shows is what the current build renders rather than an
219+
older one. It refuses to run if the mockup's theme bootstrap has changed,
220+
because the failure mode otherwise is two paper captures, one of them labelled
221+
graphite.
208222

209223
Two findings worth keeping:
210224

211225
- A contrast audit run while the browser pane was not compositing reported a
212226
clean result that **meant nothing** — the page had laid out at its narrow
213227
breakpoint, where the palette and inspector are `display: none` and were never
214-
checked. Verify the viewport before trusting the audit.
228+
checked. It happened a second time on a freshly opened tab, which does not
229+
inherit the previous tab's emulated size. The audit now asserts
230+
`innerWidth === 1440` and throws otherwise, and reports how many nodes it
231+
inspected so a zero is readable as a pass rather than as silence.
215232
- The design detector once ran degraded and returned `[]`. An empty result from
216233
a degraded tool is an undercount, not a pass.
217234

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@
3636
3737
---
3838

39+
## The interface
40+
41+
**Not a running application — a build of the planned one.** The schematic, the component palette,
42+
the parameter panel and every number in the results dock come from a real engine run, exported by
43+
[`examples/export_ui_data.py`](examples/export_ui_data.py); what is missing is the session server
44+
that would let you press Run and get a new one. Open
45+
[`docs/ui-mockup.html`](docs/ui-mockup.html) in a browser to click through it, and
46+
[DESIGN.md](DESIGN.md) for why it looks like this.
47+
48+
![The OpenOptiSim schematic editor on its paper ground](docs/images/studio-paper.png)
49+
50+
It ships two grounds and defaults to paper, because a schematic is a document before it is a
51+
screen and its plots leave the tool for reports and papers. Graphite is one click away:
52+
53+
![The same editor on its graphite ground](docs/images/studio-graphite.png)
54+
55+
Every wire colour is a wavelength rather than a preference — optical C-band cyan, electrical amber,
56+
binary slate, symbol violet, metric magenta — so a glance at a link tells you what travels down it.
57+
A typed-port system that refuses invalid wiring at edit time is worth nothing if the types are
58+
invisible.
59+
60+
---
61+
3962
## Try it
4063

4164
```bash

docs/capture_screenshots.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Regenerate the README's interface screenshots from the mockup.
2+
3+
Run after any change to ``docs/ui-mockup.html``, so the front page shows what the
4+
current build renders rather than an older one. The README is the first thing a
5+
visitor sees; a screenshot that has drifted from the build is worse than none,
6+
because it is wrong rather than merely missing.
7+
8+
python docs/capture_screenshots.py
9+
10+
Both grounds are captured, because the interface ships both and the README says
11+
so. Graphite is taken from a throwaway copy whose theme bootstrap is forced —
12+
the page otherwise defaults to paper and headless Chrome has no session storage
13+
to read a choice out of.
14+
15+
Chrome is used directly rather than through Playwright or Selenium: it is already
16+
on every machine that would run this, and a screenshot needs none of what a
17+
driver adds. ``--virtual-time-budget`` matters — the plots are drawn on a canvas
18+
from a ``requestAnimationFrame`` callback, so a capture taken too early gets an
19+
empty dock.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import shutil
25+
import subprocess
26+
import sys
27+
import tempfile
28+
from pathlib import Path
29+
30+
DOCS = Path(__file__).parent
31+
MOCKUP = DOCS / "ui-mockup.html"
32+
OUTPUT = DOCS / "images"
33+
34+
#: The viewport the design is specified at. DESIGN.md's layout numbers and every
35+
#: contrast audit are quoted at this width; capturing at another one would show a
36+
#: layout nothing else in the project describes.
37+
WIDTH, HEIGHT = 1440, 900
38+
39+
#: Long enough for the canvas plots to be drawn. They are not on the critical
40+
#: rendering path, so a shorter budget yields a blank results dock.
41+
TIME_BUDGET_MS = 4000
42+
43+
THEME_DEFAULT = 'sessionStorage.getItem("oosim-theme") || "light"'
44+
45+
CANDIDATES = (
46+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
47+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
48+
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
49+
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
50+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
51+
"google-chrome",
52+
"chromium",
53+
)
54+
55+
56+
def find_browser() -> str:
57+
for candidate in CANDIDATES:
58+
if Path(candidate).is_file():
59+
return candidate
60+
found = shutil.which(candidate)
61+
if found:
62+
return found
63+
raise SystemExit(
64+
"no Chrome or Edge found; install one, or add its path to CANDIDATES in this file"
65+
)
66+
67+
68+
def capture(browser: str, page: Path, destination: Path) -> None:
69+
destination.parent.mkdir(parents=True, exist_ok=True)
70+
subprocess.run(
71+
[
72+
browser,
73+
"--headless",
74+
"--disable-gpu",
75+
"--hide-scrollbars",
76+
"--force-device-scale-factor=1",
77+
f"--window-size={WIDTH},{HEIGHT}",
78+
f"--virtual-time-budget={TIME_BUDGET_MS}",
79+
f"--screenshot={destination}",
80+
page.resolve().as_uri(),
81+
],
82+
check=True,
83+
capture_output=True,
84+
)
85+
if not destination.is_file():
86+
raise SystemExit(f"{browser} reported success but wrote no file to {destination}")
87+
88+
89+
def main() -> None:
90+
if not MOCKUP.is_file():
91+
raise SystemExit(f"{MOCKUP} not found")
92+
browser = find_browser()
93+
source = MOCKUP.read_text(encoding="utf-8")
94+
if THEME_DEFAULT not in source:
95+
raise SystemExit(
96+
"the mockup's theme bootstrap has changed; update THEME_DEFAULT to match, "
97+
"or the graphite capture will silently be a second paper one"
98+
)
99+
100+
capture(browser, MOCKUP, OUTPUT / "studio-paper.png")
101+
102+
with tempfile.TemporaryDirectory() as work:
103+
forced = Path(work) / "graphite.html"
104+
forced.write_text(source.replace(THEME_DEFAULT, '"dark"'), encoding="utf-8")
105+
capture(browser, forced, OUTPUT / "studio-graphite.png")
106+
107+
for name in ("studio-paper.png", "studio-graphite.png"):
108+
path = OUTPUT / name
109+
print(f"{path.relative_to(DOCS.parent)} {path.stat().st_size / 1024:.0f} kB")
110+
111+
112+
if __name__ == "__main__":
113+
sys.exit(main())

docs/images/studio-graphite.png

159 KB
Loading

docs/images/studio-paper.png

158 KB
Loading

docs/ui-mockup.html

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@
705705
</div>
706706
<div class="canvas-wrap" id="canvas-wrap">
707707
<div class="canvas-grid"></div>
708-
<svg id="graph" viewBox="0 0 1000 420" preserveAspectRatio="xMidYMid meet"
708+
<svg id="graph" viewBox="0 0 1000 530" preserveAspectRatio="xMidYMid meet"
709709
role="img" aria-label="Optical link schematic"></svg>
710710
<div class="canvas-badge">
711711
<span>Ports</span>
@@ -14694,23 +14694,23 @@
1469414694
into two electrical drives, an optical field, two photocurrents back, and
1469514695
symbols out again. Every one of the five port colours appears on it. */
1469614696
const NODES = [
14697-
{ id: "ref", type: "QAMMapper", x: 168, y: 25, label: "ref", sub: "absolute" },
14698-
{ id: "tx", type: "CWLaser", x: 306, y: 25, label: "tx", sub: "+2 dBm" },
14699-
{ id: "lo", type: "CWLaser", x: 444, y: 25, label: "lo", sub: "+10 dBm" },
14700-
{ id: "pm", type: "PowerMeter", x: 720, y: 25, label: "pm", sub: "−27.9 dBm" },
14701-
{ id: "prbs", type: "PRBSGenerator", x: 30, y: 140, label: "prbs", sub: "PRBS-23" },
14702-
{ id: "map", type: "QAMMapper", x: 168, y: 140, label: "map", sub: "16-QAM diff" },
14703-
{ id: "drv", type: "IQDriver", x: 306, y: 140, label: "drv", sub: "RRC β 0.2" },
14704-
{ id: "mod", type: "IQModulator", x: 444, y: 140, label: "mod", sub: "Vπ 4 V" },
14705-
{ id: "fib", type: "Fiber", x: 582, y: 140, label: "fib", sub: "80 km SMF" },
14706-
{ id: "rx", type: "CoherentReceiver", x: 720, y: 140, label: "rx", sub: "0.8 A/W" },
14707-
{ id: "cdc", type: "DispersionCompensator", x: 30, y: 295, label: "cdc", sub: "1360 ps/nm" },
14708-
{ id: "smp", type: "IQSampler", x: 168, y: 295, label: "smp", sub: "matched" },
14709-
{ id: "cr", type: "CarrierRecovery", x: 306, y: 295, label: "cr", sub: "64-sym BPS" },
14710-
{ id: "dec", type: "DifferentialDecoder", x: 444, y: 295, label: "dec", sub: "quadrant" },
14711-
{ id: "cd", type: "ConstellationDiagram", x: 582, y: 295, label: "cd", sub: "96 × 96" },
14712-
{ id: "vsa", type: "ConstellationAnalyzer", x: 720, y: 295, label: "vsa", sub: "EVM 7.3 %" },
14713-
{ id: "ber", type: "ConstellationAnalyzer", x: 858, y: 295, label: "ber", sub: "0 errors" }
14697+
{ id: "ref", type: "QAMMapper", x: 168, y: 30, label: "ref", sub: "absolute" },
14698+
{ id: "tx", type: "CWLaser", x: 306, y: 30, label: "tx", sub: "+2 dBm" },
14699+
{ id: "lo", type: "CWLaser", x: 444, y: 30, label: "lo", sub: "+10 dBm" },
14700+
{ id: "pm", type: "PowerMeter", x: 720, y: 30, label: "pm", sub: "−27.9 dBm" },
14701+
{ id: "prbs", type: "PRBSGenerator", x: 30, y: 220, label: "prbs", sub: "PRBS-23" },
14702+
{ id: "map", type: "QAMMapper", x: 168, y: 220, label: "map", sub: "16-QAM diff" },
14703+
{ id: "drv", type: "IQDriver", x: 306, y: 220, label: "drv", sub: "RRC β 0.2" },
14704+
{ id: "mod", type: "IQModulator", x: 444, y: 220, label: "mod", sub: "Vπ 4 V" },
14705+
{ id: "fib", type: "Fiber", x: 582, y: 220, label: "fib", sub: "80 km SMF" },
14706+
{ id: "rx", type: "CoherentReceiver", x: 720, y: 220, label: "rx", sub: "0.8 A/W" },
14707+
{ id: "cdc", type: "DispersionCompensator", x: 30, y: 410, label: "cdc", sub: "1360 ps/nm" },
14708+
{ id: "smp", type: "IQSampler", x: 168, y: 410, label: "smp", sub: "matched" },
14709+
{ id: "cr", type: "CarrierRecovery", x: 306, y: 410, label: "cr", sub: "64-sym BPS" },
14710+
{ id: "dec", type: "DifferentialDecoder", x: 444, y: 410, label: "dec", sub: "quadrant" },
14711+
{ id: "cd", type: "ConstellationDiagram", x: 582, y: 410, label: "cd", sub: "96 × 96" },
14712+
{ id: "vsa", type: "ConstellationAnalyzer", x: 720, y: 410, label: "vsa", sub: "EVM 7.3 %" },
14713+
{ id: "ber", type: "ConstellationAnalyzer", x: 858, y: 410, label: "ber", sub: "0 errors" }
1471414714
];
1471514715
const EDGES = [
1471614716
{ from: ["prbs", "out"], to: ["map", "in"], type: "binary" },

0 commit comments

Comments
 (0)