gds-lens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ // The element the engine mounts into, handed over between <gds-lens>'s
2
+ // connectedCallback and viewer.js.
3
+ //
4
+ // It exists because viewer.js does its work as a module body rather than in a
5
+ // mount function: 172 top-level statements spread through the file, holding
6
+ // the renderer's state in module scope. Wrapping all of that in a function
7
+ // would mean indenting the entire file for no behavioural gain, so the element
8
+ // defers it with a dynamic import instead and leaves the target here for it to
9
+ // pick up. Passing the element rather than having viewer.js query for it also
10
+ // keeps this correct when the <gds-lens> is itself inside someone else's
11
+ // shadow root, where document.querySelector would not find it.
12
+
13
+ let target = null;
14
+
15
+ export function setMountTarget(element) {
16
+ target = element;
17
+ }
18
+
19
+ export function takeMountTarget() {
20
+ const element = target;
21
+ target = null;
22
+ return element;
23
+ }
@@ -0,0 +1,105 @@
1
+ <!-- lil-gui mounts its panel here rather than at document.body, so the
2
+ whole control surface lives inside the shadow root. -->
3
+ <div id="guiHost"></div>
4
+ <div id="loadingOverlay" role="status" aria-live="polite">
5
+ <div id="loadingBarTrack"><div id="loadingBarFill"></div></div>
6
+ <div id="loadingPhase">Loading layout...</div>
7
+ <div id="loadingPercent"></div>
8
+ </div>
9
+ <!-- role="alert" rather than a polite live region: this replaces the
10
+ view entirely, so it is the only thing left to read. -->
11
+ <div id="loadError" role="alert"></div>
12
+ <div id="reloadProgress" class="hidden" role="status" aria-live="polite">
13
+ <div id="reloadBarTrack"><div id="reloadBarFill"></div></div>
14
+ <div id="reloadLabel">Reloading...</div>
15
+ </div>
16
+ <div id="staleBanner" class="hidden" role="status">
17
+ <span id="staleText">A newer version of this file is on disk.</span>
18
+ <button id="staleReloadBtn" type="button">Reload</button>
19
+ <button id="staleAlwaysBtn" type="button" class="secondary"
20
+ title="Reload now, and automatically whenever this file changes from now on">Always</button>
21
+ <button id="staleDismiss" type="button" class="icon-only"
22
+ aria-label="Dismiss until the next change"
23
+ title="Dismiss until the next change">✕</button>
24
+ </div>
25
+ <!-- tabindex so the keyboard handlers in viewer.js are reachable without
26
+ a pointer; labelled because a bare <canvas> announces nothing. -->
27
+ <canvas id="glCanvas" tabindex="0" role="img"
28
+ aria-label="Layout view. Drag to pan, scroll to zoom."></canvas>
29
+
30
+ <!-- Cell hierarchy: rows are built by renderHierarchy in viewer.js from
31
+ the tree parseGdsToLayers hands back, lazily as branches open. -->
32
+ <div id="hierarchyPanel" class="hidden">
33
+ <div id="hierarchyHeader">
34
+ <span id="hierarchyTitle">Hierarchy</span>
35
+ <span id="hierarchyCount"></span>
36
+ <button id="hierarchyHide" type="button" class="icon-only"
37
+ aria-label="Hide the hierarchy tree"
38
+ title="Hide the hierarchy tree (H)">✕</button>
39
+ </div>
40
+ <!-- Find (see the search block in viewer.js): one box over two
41
+ haystacks, the design's cell names and its own TEXT labels.
42
+ Results take the tree's place while a query is up.
43
+
44
+ Collapsed by default, and folded away behind one row rather than
45
+ sitting open above the tree: the panel is here to browse the
46
+ hierarchy, and two more rows of chrome over it are two fewer rows
47
+ of the thing being browsed. "/" opens it and lands in the box, so
48
+ the fold costs nothing to whoever is actually searching. -->
49
+ <div id="hierarchyFind">
50
+ <button id="hierarchyFindToggle" type="button"
51
+ aria-expanded="false" aria-controls="hierarchySearch"
52
+ title="Find a cell by name, or a label by its text (/)">
53
+ <span id="hierarchyFindTwisty">▸</span>
54
+ <span id="hierarchyFindLabel">Find</span>
55
+ <span id="hierarchySearchCount"></span>
56
+ </button>
57
+ <div id="hierarchySearch" class="hidden">
58
+ <input id="hierarchySearchInput" type="text" placeholder="cell name"
59
+ spellcheck="false" autocomplete="off"
60
+ title="Find a cell by name, or a label by its text (/ focuses this box)">
61
+ <div id="hierarchyScope">
62
+ <button id="hierarchyScopeCells" type="button" aria-pressed="true"
63
+ title="Search the design's cell names">Cells</button>
64
+ <button id="hierarchyScopeLabels" type="button" aria-pressed="false"
65
+ title="Search the layout's own TEXT labels">Labels</button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <div id="hierarchyTree" role="tree" aria-label="Cell hierarchy"></div>
70
+ <div id="hierarchyResults" class="hidden" role="listbox" aria-label="Search results"></div>
71
+ </div>
72
+ <button id="hierarchyShowBtn" type="button" class="hidden"
73
+ aria-expanded="false" aria-controls="hierarchyPanel"
74
+ title="Show the design's cell hierarchy (H)">Hierarchy</button>
75
+
76
+ <div id="debugPanel" class="hidden">
77
+ <div id="debugPanelHeader">
78
+ <span>Debug tools</span>
79
+ <button id="debugCopyBtn" type="button">Copy log</button>
80
+ </div>
81
+ <div id="debugBody">
82
+ <!-- Engine readout, written from renderer.cpp (set_inner_html("ui", ...)). -->
83
+ <div id="ui"></div>
84
+ <div id="debugLog"></div>
85
+ </div>
86
+ </div>
87
+ <button id="debugToggleBtn" type="button"
88
+ aria-expanded="false" aria-controls="debugPanel">Debug tools</button>
89
+
90
+ <div id="viewReadout">
91
+ <div id="copyToast" class="hidden" role="status" aria-live="polite"></div>
92
+ <div id="scaleContainer">
93
+ <div id="scaleLabel">0 μm</div>
94
+ <div id="scaleBar" style="width: 100px;"></div>
95
+ </div>
96
+ <div id="coordReadout" class="hidden"></div>
97
+ </div>
98
+
99
+ <div id="measureLabels"></div>
100
+
101
+ <div id="canvasMenu" class="hidden" role="menu">
102
+ <button id="canvasMenuCopy" type="button" role="menuitem">
103
+ <span>Copy coordinate</span><span class="menu-value"></span>
104
+ </button>
105
+ </div>
package/src/viewer.css ADDED
@@ -0,0 +1,469 @@
1
+ /* ---- Theme tokens ----
2
+ Every color in this file comes from here, because the viewer follows
3
+ the embedder's light/dark theme and each one needs a counterpart.
4
+ Dark is the default: it's what the page paints as before any script
5
+ runs, and it's the far more common editor theme.
6
+
7
+ The light values ride on a single .theme-light class, which viewer.js's
8
+ theme block sets: it asks the host which theme is current and falls
9
+ back to the OS preference when the host has no opinion (see
10
+ applyTheme there). An embedder can also just set the class itself.
11
+ The canvas is not covered by any of this -- what the renderer draws is
12
+ themed in renderer.cpp (see setTheme). */
13
+ :host {
14
+ --bg: #0c0c0d;
15
+ --fg: #ffffff; /* ink over the canvas: scale bar, ruler readout */
16
+ --fg-dim: #a5a5a5;
17
+ --fg-faint: #808080;
18
+ --surface: #2a2a2c; /* small chrome fills: bar tracks, buttons */
19
+ --surface-fg: #dddddd;
20
+ --scrim: rgba(0, 0, 0, 0.88); /* debug panel, over the canvas */
21
+ --label-scrim: rgba(0, 0, 0, 0.75); /* ruler readout, over geometry */
22
+ --chip-scrim: rgba(12, 12, 13, 0.85); /* reload chip, over geometry */
23
+ --border: #333333;
24
+ --border-strong: #555555;
25
+ --accent: #4a9eff;
26
+ --accent-fg: #06121f; /* text on an --accent fill */
27
+ --accent-text: #cfe6ff; /* --accent as text, on --chip-scrim */
28
+ --accent-border: #2a3f57;
29
+ --danger: #ff4d4d;
30
+ --danger-dim: #ff9a9a;
31
+ --select-bg: #5a1f1f; /* selected marker's row */
32
+ --select-fg: #ffd9d9;
33
+ --error-bg: #2a1416;
34
+ --error-border: #7a2b2b;
35
+ --error-fg: #ffd9d9;
36
+ --warn-bg: #4a3410; /* "newer version on disk" banner */
37
+ --warn-border: #7a5a1c;
38
+ --warn-border-strong: #a37a26;
39
+ --warn-fg: #ffe9c2;
40
+ --warn-fg-dim: #e5cfa4;
41
+ --warn-btn-fg: #fff6e5;
42
+ --log-fg: #7fff7f;
43
+ --log-err: #ff7f7f;
44
+ /* lil-gui's own palette (mapped onto its variable names below). */
45
+ --panel-bg: #1f1f1f;
46
+ --panel-fg: #ebebeb;
47
+ --panel-title-bg: #111111;
48
+ --panel-widget: #424242;
49
+ --panel-widget-hover: #4f4f4f;
50
+ --panel-widget-focus: #595959;
51
+ }
52
+ :host(.theme-light) {
53
+ --bg: #fafafb;
54
+ --fg: #17171a;
55
+ --fg-dim: #56565c;
56
+ --fg-faint: #6b6b73; /* matches the dark side's contrast, not its lightness */
57
+ --surface: #e4e4e8;
58
+ --surface-fg: #2c2c2e;
59
+ --scrim: rgba(250, 250, 251, 0.93);
60
+ --label-scrim: rgba(255, 255, 255, 0.85);
61
+ --chip-scrim: rgba(255, 255, 255, 0.9);
62
+ --border: #d2d2d8;
63
+ --border-strong: #b4b4bc;
64
+ --accent: #1f6fd0;
65
+ --accent-fg: #ffffff;
66
+ --accent-text: #14538f;
67
+ --accent-border: #a8c8e8;
68
+ --danger: #b3261e;
69
+ --danger-dim: #d0574e;
70
+ --select-bg: #ffdcdc;
71
+ --select-fg: #7a1f1f;
72
+ --error-bg: #fdeceb;
73
+ --error-border: #dda3a0;
74
+ --error-fg: #7d201c;
75
+ --warn-bg: #fdf3d8;
76
+ --warn-border: #e0c07a;
77
+ --warn-border-strong: #c9a153;
78
+ --warn-fg: #5c431a;
79
+ --warn-fg-dim: #7d6231;
80
+ --warn-btn-fg: #3f2d0e;
81
+ --log-fg: #157f2a;
82
+ --log-err: #b3261e;
83
+ --panel-bg: #f4f4f6;
84
+ --panel-fg: #2c2c2e;
85
+ --panel-title-bg: #e4e4e8;
86
+ --panel-widget: #d8d8de;
87
+ --panel-widget-hover: #c8c8d0;
88
+ --panel-widget-focus: #babac2;
89
+ }
90
+ /* lil-gui's palette is its own set of custom properties, so the panel
91
+ themes by re-pointing those at the --panel-* tokens above rather
92
+ than by restyling any of its rows. Mapped here on .lil-gui rather
93
+ than on :root because lil-gui's own .lil-gui rule sets them, and a
94
+ rule matching the element itself beats an inherited value. (Source
95
+ order settles the tie: lil-gui inserts its stylesheet *before* the
96
+ first <style> already in <head> -- i.e. this one -- specifically so
97
+ page styles outrank its defaults.) */
98
+ .lil-gui {
99
+ --background-color: var(--panel-bg);
100
+ --text-color: var(--panel-fg);
101
+ --title-background-color: var(--panel-title-bg);
102
+ --title-text-color: var(--panel-fg);
103
+ --widget-color: var(--panel-widget);
104
+ --hover-color: var(--panel-widget-hover);
105
+ --focus-color: var(--panel-widget-focus);
106
+ --number-color: var(--accent);
107
+ }
108
+ /* A custom element is display:inline by default and would collapse to
109
+ zero height. The host owns the box; everything inside positions
110
+ against it rather than against the page. */
111
+ :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden;
112
+ background: var(--bg); user-select: none; color: var(--fg); }
113
+ canvas { width: 100%; height: 100%; display: block; }
114
+ /* The engine readout (polygon/label counts + the per-frame #renderStats
115
+ span, both written from renderer.cpp). It lives inside #debugPanel
116
+ rather than floating over the canvas on its own: it's debug-only
117
+ either way, and two separate debug surfaces meant the same command
118
+ lit up two different corners of the screen. */
119
+ #ui { width: 320px; flex: none; overflow-y: auto; padding: 6px 8px; border-right: 1px solid var(--border);
120
+ color: var(--fg-dim); font-family: monospace; font-size: 11px; line-height: 1.5; user-select: text; }
121
+ /* The debug entry point is hidden until the "GDSLens: Toggle Debug
122
+ Tools" command adds .debug to <body>. */
123
+ #debugToggleBtn { display: none; }
124
+ :host(.debug) #debugToggleBtn { display: block; }
125
+ /* Bottom-right stack of view readouts: the scale bar, with the
126
+ pointer's world coordinate under it. One anchored column rather than
127
+ two separately positioned boxes, so the coordinate row -- whose width
128
+ varies with the number in it -- can't drift into or away from the bar
129
+ above it. */
130
+ #viewReadout { position: absolute; bottom: 20px; right: 20px; z-index: 10; pointer-events: none;
131
+ display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
132
+ #scaleContainer { font-family: monospace; font-size: 11px; color: var(--fg); text-align: center; }
133
+ /* Pointer coordinate, rewritten by update_coord_readout (renderer.cpp)
134
+ on every move and every camera change; .hidden while the pointer is
135
+ off the canvas. Unlike the scale bar it's a run of text over
136
+ arbitrary geometry, so it carries the same scrim the ruler label
137
+ does rather than relying on an outline. */
138
+ #coordReadout { font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim);
139
+ border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 6px;
140
+ white-space: nowrap; }
141
+ #coordReadout.hidden { display: none; }
142
+ /* Confirmation that the canvas menu's Copy coordinate reached the
143
+ clipboard (see the contextmenu handler in viewer.js). Same scrim as
144
+ the readout it answers for. It sits first in the column on purpose:
145
+ #viewReadout is anchored by its bottom edge, so a line appearing at
146
+ the top leaves the scale bar and the coordinate exactly where they
147
+ were -- one added at the bottom would shove both up mid-read. */
148
+ #copyToast { font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim);
149
+ border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 6px;
150
+ white-space: nowrap; }
151
+ #copyToast.hidden { display: none; }
152
+ /* Right-click menu on the canvas (see the contextmenu handler in
153
+ viewer.js). VS Code's own webview menu is suppressed for it -- the
154
+ preload bails out of showing one when the page has already called
155
+ preventDefault -- so this has to look like a menu on its own. It
156
+ borrows the panel's palette rather than the readouts' scrim: it sits
157
+ over the layout but it's chrome, not an annotation of what's under
158
+ it. Above the debug panel (z 41), since a menu is the most recent
159
+ thing the user asked for. */
160
+ #canvasMenu { position: absolute; z-index: 50; min-width: 190px; padding: 4px;
161
+ background: var(--panel-bg); color: var(--panel-fg);
162
+ border: 1px solid var(--border-strong); border-radius: 4px;
163
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); }
164
+ #canvasMenu.hidden { display: none; }
165
+ #canvasMenu button { display: flex; align-items: baseline; gap: 14px; width: 100%;
166
+ background: none; border: 0; border-radius: 3px; padding: 4px 8px;
167
+ font-family: inherit; font-size: 12px; color: inherit;
168
+ text-align: left; white-space: nowrap; cursor: pointer; }
169
+ #canvasMenu button:hover, #canvasMenu button:focus { background: var(--accent); color: var(--accent-fg); outline: none; }
170
+ /* The coordinate itself, shown in the readout's monospace so it reads as
171
+ the value being copied rather than as a menu shortcut. */
172
+ #canvasMenu .menu-value { margin-left: auto; font-family: monospace; font-size: 11px; color: var(--fg-dim); }
173
+ #canvasMenu button:hover .menu-value, #canvasMenu button:focus .menu-value { color: inherit; }
174
+ /* The bar is drawn in the ink color over an outline of the background,
175
+ so it stays readable whichever of the two it happens to sit on top of
176
+ (it floats over the geometry, not over a panel). */
177
+ #scaleBar { height: 4px; background: var(--fg); border: 1px solid var(--bg); margin-top: 4px; }
178
+ /* Ruler readouts: one .measure-label per ruler, created and
179
+ repositioned to its measurement's midpoint on every redraw by
180
+ update_measure_labels (renderer.cpp). The translate lifts each one
181
+ just above its line so it doesn't cover what's being measured.
182
+ #measureLabels is a plain (unpositioned) container, so the labels
183
+ inside it still position against the page the way the single one
184
+ used to. */
185
+ #measureLabels .measure-label { position: absolute; transform: translate(-50%, -130%); font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim); border: 1px solid var(--border-strong); border-radius: 3px; padding: 3px 7px; text-align: center; line-height: 1.4; white-space: nowrap; z-index: 15; pointer-events: none; }
186
+ /* lil-gui floats its own panel only when it appends to document.body,
187
+ by adding .lil-auto-place (position:fixed, right:15px, z 1001). We
188
+ pass a container instead so the panel lives inside the shadow root,
189
+ which skips that class entirely -- so without this the panel is a
190
+ plain block in normal flow, spanning the full width and pushing the
191
+ canvas down. Positioning it here also fixes two things lil-gui's own
192
+ rule got wrong for a component: absolute rather than fixed, so the
193
+ panel stays inside the component's box instead of pinning itself to
194
+ the viewport when embedded in a page; and z-index below the loading
195
+ overlay (30) and debug panel (40) rather than lil-gui's 1001. */
196
+ #guiHost { position: absolute; top: 0; right: 15px; max-height: 100%; z-index: 20; display: flex; }
197
+ #guiHost > .lil-gui.lil-root { max-height: 100%; }
198
+ /* Checkbox rows (layer list + top-level toggles): lil-gui gives the
199
+ label a 45% min-width and no overflow handling, so a long name like
200
+ "250/74 – SIN_WG_S" would push into the checkbox column. Widen the
201
+ label (the checkbox needs nowhere near half the row) and cut it
202
+ character-level with an ellipsis. */
203
+ .lil-gui .lil-controller.lil-boolean > .lil-name { min-width: 78%; width: 78%; overflow: hidden; text-overflow: ellipsis; }
204
+ /* ---- Layer rows (see addLayerRow in viewer.js) ----
205
+ A layer row carries two more things than the plain boolean above: its
206
+ shape count and a solo button, both appended after lil-gui's own
207
+ name/widget pair. .lil-controller is already a flex row, so they only
208
+ need flex:none to sit at the right edge; the name gives up the width
209
+ they take (this rule follows the 78% one above deliberately -- same
210
+ specificity, so source order decides). */
211
+ .lil-gui .lil-controller.layer-row > .lil-name { min-width: 58%; width: 58%; }
212
+ .lil-gui .lil-controller.layer-row .layer-count { flex: none; padding-left: 4px;
213
+ color: var(--fg-faint); font-size: 10px; }
214
+ .lil-gui .lil-controller.layer-row .layer-solo { flex: none; width: 16px; text-align: center;
215
+ margin-left: 4px; border-radius: 2px;
216
+ color: var(--fg-faint); font-size: 10px; cursor: pointer; }
217
+ .lil-gui .lil-controller.layer-row .layer-solo:hover { color: var(--fg); background: var(--panel-widget-hover); }
218
+ /* The soloed layer's own row, so it's clear which of the hidden
219
+ hundred is the one still on screen. */
220
+ .lil-gui .lil-controller.layer-row.layer-soloed .layer-solo { color: var(--accent-fg); background: var(--accent); }
221
+ /* Filter box at the top of the Layers folder. lil-gui styles the inputs
222
+ belonging to its own controllers; this one is hand-built, so it
223
+ borrows the same widget fill. */
224
+ .lil-gui .lil-controller.layer-filter-row input { width: 100%; box-sizing: border-box;
225
+ font-family: inherit; font-size: var(--input-font-size);
226
+ background: var(--panel-widget); color: var(--panel-fg);
227
+ border: none; border-radius: var(--widget-border-radius);
228
+ height: var(--widget-height); padding: 0 6px; }
229
+ .lil-gui .lil-controller.layer-filter-row input:focus { background: var(--panel-widget-focus); outline: none; }
230
+ /* Interaction-mode row (Pan | Measure), hand-built in viewer.js since
231
+ lil-gui has no segmented-control type. Because the row carries
232
+ lil-gui's own classes, its button theming (widget color, radius,
233
+ height, :hover/:active) already applies -- all that's left is
234
+ splitting the widget in two and filling in the selected mode. The
235
+ name column is narrower than lil-gui's 45% default to leave both
236
+ buttons a readable width. */
237
+ .lil-gui .lil-controller.mode-row .lil-name { min-width: 30%; }
238
+ .lil-gui .lil-controller.mode-row .mode-widget { gap: 4px; }
239
+ .lil-gui .lil-controller.mode-row .mode-widget button { flex: 1; }
240
+ /* :hover/:active spelled out too -- lil-gui's own button:hover rule
241
+ outranks a bare .mode-active, which would grey out the selected
242
+ mode as soon as the pointer crossed it. */
243
+ .lil-gui .lil-controller.mode-row .mode-widget button.mode-active,
244
+ .lil-gui .lil-controller.mode-row .mode-widget button.mode-active:hover,
245
+ .lil-gui .lil-controller.mode-row .mode-widget button.mode-active:active { background: var(--accent); color: var(--accent-fg); font-weight: bold; }
246
+ /* Loaded-.lyp chip: the row shows the filename with an ✕ on the right
247
+ (injected by setLypChip in viewer.js) to unload it. */
248
+ .lil-gui .lil-controller.lyp-loaded { position: relative; }
249
+ .lil-gui .lil-controller.lyp-loaded .lil-name { padding-right: 22px; }
250
+ .lil-gui .lil-controller.lyp-loaded .lyp-unload { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--danger-dim); cursor: pointer; font-size: 12px; z-index: 5; }
251
+ .lil-gui .lil-controller.lyp-loaded .lyp-unload:hover { color: var(--danger); }
252
+ /* Saved-view rows (see renderNamedViews in viewer.js): a full-width
253
+ button that restores the view, with an ✕ on the right that deletes
254
+ it -- the same shape as the loaded-file chips above, and the same
255
+ reason for spelling it out separately (lil-gui has no row that
256
+ carries a second action). */
257
+ .lil-gui .lil-controller.view-row { position: relative; }
258
+ .lil-gui .lil-controller.view-row .lil-name { padding-right: 22px; }
259
+ .lil-gui .lil-controller.view-row .view-delete { position: absolute; right: 8px; top: 50%;
260
+ transform: translateY(-50%); color: var(--danger-dim);
261
+ cursor: pointer; font-size: 12px; z-index: 5; }
262
+ .lil-gui .lil-controller.view-row .view-delete:hover { color: var(--danger); }
263
+ /* Marker browser (see renderMarkerBrowser in viewer.js): the selected
264
+ violation's row, and the disabled "… N more" overflow row. The row
265
+ is a full-width <button> in lil-gui, so tint the button itself
266
+ (!important also beats lil-gui's :hover/:active backgrounds). */
267
+ .lil-gui .lil-controller.marker-selected button { background: var(--select-bg) !important; }
268
+ .lil-gui .lil-controller.marker-selected .lil-name { color: var(--select-fg); }
269
+ .lil-gui .lil-controller.marker-more-row { opacity: 0.55; pointer-events: none; }
270
+ /* ---- Hierarchy tree (see renderHierarchy in viewer.js) ----
271
+ The design's cell tree, down the left edge. Like the lil-gui panel
272
+ on the right it floats over the canvas rather than taking width
273
+ from it: the canvas stays the full window, so nothing about the
274
+ renderer's sizing, hit-testing or camera has to know this panel
275
+ exists. It outranks both progress UIs (z 25/26) so a reload doesn't
276
+ draw its hairline bar across the tree, but stays under the
277
+ full-screen loading overlay (z 30) and the debug panel (z 40). */
278
+ #hierarchyPanel { position: absolute; top: 0; left: 0; bottom: 0; width: 260px; z-index: 27;
279
+ box-sizing: border-box; display: flex; flex-direction: column;
280
+ background: var(--scrim); border-right: 1px solid var(--border);
281
+ font-family: monospace; font-size: 11px; color: var(--surface-fg); }
282
+ #hierarchyPanel.hidden { display: none; }
283
+ #hierarchyHeader { display: flex; align-items: center; gap: 8px; flex: none; padding: 6px 8px;
284
+ color: var(--fg-dim); border-bottom: 1px solid var(--border); }
285
+ #hierarchyTitle { flex: 1; }
286
+ #hierarchyCount { color: var(--fg-faint); }
287
+ /* Same icon-only treatment as #staleDismiss: a real button, styled
288
+ back down to the bare glyph it was before. */
289
+ #hierarchyHide { background: none; border: 0; font: inherit; color: inherit;
290
+ cursor: pointer; opacity: 0.7; padding: 0 2px; }
291
+ #hierarchyHide:hover { opacity: 1; }
292
+ #hierarchyTree { flex: 1; overflow: auto; padding: 4px 0 8px; }
293
+ /* One row per node. The whole row frames its cell on click, so it's
294
+ the click target; only the left indent varies with depth (set
295
+ inline, since it's per-row). */
296
+ .hier-row { display: flex; align-items: center; gap: 4px; padding: 2px 8px 2px 0;
297
+ white-space: nowrap; cursor: pointer; }
298
+ .hier-row:hover { background: var(--surface); }
299
+ .hier-row.hier-selected, .hier-row.hier-selected:hover { background: var(--accent); color: var(--accent-fg); }
300
+ /* A cell with no geometry anywhere under it -- there's nothing to
301
+ frame, so the row only selects. */
302
+ .hier-row.hier-boxless { opacity: 0.55; cursor: default; }
303
+ .hier-twisty { flex: none; width: 12px; text-align: center; color: var(--fg-faint); }
304
+ /* flex + min-width:0 is what lets a long cell name actually truncate:
305
+ a flex item's default min-width is its content, and a cell name is
306
+ one unbreakable word, so without this the row grows and the panel
307
+ scrolls sideways instead. Taking the leftover width also pins the
308
+ placement count to the right edge. */
309
+ .hier-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
310
+ /* Placement count ("×64"), shown only when the cell is placed more
311
+ than once by its parent. */
312
+ .hier-count { flex: none; color: var(--fg-faint); }
313
+ .hier-row.hier-selected .hier-twisty, .hier-row.hier-selected .hier-count { color: var(--accent-fg); }
314
+ .hier-children.hidden { display: none; }
315
+ /* ---- Find (see the search block in viewer.js) ----
316
+ Sits between the header and the tree: it's a way into the same
317
+ panel, not a second panel. One row while it's closed, which is how
318
+ it starts -- see the markup below for why -- opening to the box and
319
+ the scope pair. The pair gets its own row rather than sharing one
320
+ with the input: at 260px a field narrowed by two buttons is too
321
+ small to read a cell name in. */
322
+ #hierarchyFind { flex: none; border-bottom: 1px solid var(--border); }
323
+ /* The closed row. Styled like a tree row rather than like the panel
324
+ header, because that's what it is: something you click to open. */
325
+ #hierarchyFindToggle { display: flex; align-items: center; gap: 4px; padding: 3px 8px;
326
+ color: var(--fg-dim); cursor: pointer; white-space: nowrap;
327
+ /* A <button> now, for keyboard and screen readers: reset the
328
+ chrome and let it fill the row as the div did. */
329
+ width: 100%; background: none; border: 0; font: inherit;
330
+ text-align: left; }
331
+ #hierarchyFindToggle:hover { background: var(--surface); color: var(--fg); }
332
+ #hierarchyFindTwisty { flex: none; width: 12px; text-align: center; color: var(--fg-faint); }
333
+ #hierarchyFindLabel { flex: none; }
334
+ #hierarchySearch { padding: 0 8px 6px; display: flex; flex-direction: column; gap: 5px; }
335
+ #hierarchySearch.hidden { display: none; }
336
+ /* user-select is off for the page as a whole (dragging the canvas would
337
+ otherwise select the chrome over it), and that inherits into a text
338
+ field -- where it costs you selecting what you typed, and makes the
339
+ select() behind the "/" shortcut a no-op. */
340
+ #hierarchySearchInput { width: 100%; box-sizing: border-box; font-family: inherit; font-size: 11px;
341
+ background: var(--surface); color: var(--surface-fg);
342
+ border: 1px solid var(--border-strong); border-radius: 3px;
343
+ padding: 3px 6px; user-select: text; }
344
+ #hierarchySearchInput:focus { outline: none; border-color: var(--accent); }
345
+ #hierarchyScope { display: flex; gap: 4px; }
346
+ #hierarchyScope button { font-family: inherit; font-size: 10px; background: var(--surface);
347
+ color: var(--fg-dim); border: 1px solid var(--border-strong);
348
+ border-radius: 3px; padding: 1px 8px; cursor: pointer; }
349
+ #hierarchyScope button:hover { color: var(--fg); }
350
+ #hierarchyScope button.scope-active, #hierarchyScope button.scope-active:hover {
351
+ background: var(--accent); color: var(--accent-fg); border-color: var(--accent); font-weight: bold; }
352
+ /* A design too large for a cell tree has no cell names on this side at
353
+ all (see the omitted case in renderHierarchy), so Cells has nothing
354
+ to search -- the button says so rather than returning nothing. */
355
+ #hierarchyScope button:disabled { opacity: 0.4; cursor: default; }
356
+ /* On the toggle row rather than beside the scope pair: it's the result
357
+ of the search, so it belongs next to the thing you'd click to get
358
+ back to it. Empty whenever the fold is closed, since closing clears
359
+ the query. */
360
+ #hierarchySearchCount { flex: 1; text-align: right; color: var(--fg-faint); overflow: hidden;
361
+ text-overflow: ellipsis; }
362
+ /* Results replace the tree rather than stacking under it, so the panel
363
+ shows one list at a time; clearing the box brings the tree back with
364
+ its open branches untouched. */
365
+ #hierarchyResults { flex: 1; overflow: auto; padding: 4px 0 8px; }
366
+ #hierarchyResults.hidden, #hierarchyTree.hidden { display: none; }
367
+ /* A result row is the tree's row with the indent and twisty dropped and
368
+ a right-hand detail added (a cell's shape count, a label's layer). */
369
+ .find-row { display: flex; align-items: baseline; gap: 6px; padding: 2px 8px;
370
+ white-space: nowrap; cursor: pointer; }
371
+ .find-row:hover { background: var(--surface); }
372
+ .find-row.find-active, .find-row.find-active:hover { background: var(--accent); color: var(--accent-fg); }
373
+ .find-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
374
+ .find-meta { flex: none; color: var(--fg-faint); font-size: 10px; }
375
+ .find-row.find-active .find-meta { color: var(--accent-fg); }
376
+ /* A cell no top cell reaches (a reference cycle, or one the tree's own
377
+ caps left out) has no path to open down to, so its row only says so. */
378
+ .find-row.find-unreachable { opacity: 0.55; cursor: default; }
379
+ #hierarchyResults .find-note { padding: 6px 10px; color: var(--fg-faint); white-space: normal; line-height: 1.5; }
380
+ /* Stand-in for a tree: no cells to show, or too many to describe. */
381
+ #hierarchyTree .hier-note { padding: 6px 10px; color: var(--fg-faint); white-space: normal; line-height: 1.5; }
382
+ /* Reopens the panel after it's been hidden; sits where the panel's own
383
+ header was. Hidden while the panel is open, and while there's no
384
+ tree to reopen (viewer.js adds .hidden for that). */
385
+ #hierarchyShowBtn { position: absolute; top: 8px; left: 8px; z-index: 27; font-family: monospace;
386
+ font-size: 10px; background: var(--surface); color: var(--fg-dim);
387
+ border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px;
388
+ cursor: pointer; opacity: 0.6; }
389
+ #hierarchyShowBtn:hover { opacity: 1; }
390
+ #hierarchyShowBtn.hidden, :host(.hierarchy-open) #hierarchyShowBtn { display: none; }
391
+ /* Everything else anchored to the left edge starts after whichever of
392
+ the two is on screen -- the panel, or just the button that reopens
393
+ it (.hierarchy-available). The open rule comes second so it wins
394
+ when both classes are set. */
395
+ :host(.hierarchy-available) #staleBanner { left: 88px; }
396
+ :host(.hierarchy-open) #staleBanner { left: 268px; }
397
+ :host(.hierarchy-open) #debugToggleBtn { left: 268px; }
398
+ /* "Newer version on disk" header (see the fileChanged handler in
399
+ viewer.js). Sits above the lil-gui panel (z 20) but below the
400
+ loading overlay (z 30), since it's hidden while a load runs
401
+ anyway. */
402
+ /* Stops short of the right edge so it doesn't cover the lil-gui panel
403
+ (260px wide, top-right) that it outranks on z-index. */
404
+ #staleBanner { position: absolute; top: 0; left: 0; right: 276px; box-sizing: border-box; z-index: 25;
405
+ display: flex; align-items: center; gap: 12px; padding: 7px 12px;
406
+ font-family: monospace; font-size: 12px; color: var(--warn-fg);
407
+ background: var(--warn-bg); border-bottom: 1px solid var(--warn-border); }
408
+ #staleBanner.hidden { display: none; }
409
+ #staleText { flex: 1; }
410
+ #staleBanner button { font-family: monospace; font-size: 11px; background: var(--warn-border); color: var(--warn-btn-fg);
411
+ border: 1px solid var(--warn-border-strong); border-radius: 3px; padding: 3px 12px; cursor: pointer; }
412
+ #staleBanner button:hover { background: var(--warn-border-strong); }
413
+ /* Secondary next to Reload: same action plus a persisted setting, so it
414
+ reads as the quieter of the two. */
415
+ #staleBanner button.secondary { background: transparent; border-color: transparent;
416
+ color: var(--warn-fg-dim); padding: 3px 6px; }
417
+ #staleBanner button.secondary:hover { background: transparent; color: var(--warn-btn-fg); text-decoration: underline; }
418
+ /* An icon-only control: a real <button> for keyboard and screen
419
+ readers, styled back down to bare text. The reset is explicit
420
+ because #staleBanner button above is more specific than an id on its
421
+ own and would otherwise give this the Reload button's chrome. */
422
+ #staleBanner button#staleDismiss { background: none; border: 0; border-radius: 0;
423
+ font: inherit; color: inherit; cursor: pointer;
424
+ opacity: 0.7; padding: 0 2px; }
425
+ #staleBanner button#staleDismiss:hover { background: none; opacity: 1; }
426
+ #loadingOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; font-family: monospace; color: var(--fg-dim); background: var(--bg); z-index: 30; }
427
+ #loadingOverlay.hidden { display: none; }
428
+ #loadingBarTrack { width: 320px; height: 8px; background: var(--surface); border-radius: 4px; overflow: hidden; }
429
+ #loadingBarFill { width: 0%; height: 100%; background: var(--accent); transition: width 0.15s ease-out; }
430
+ #loadingPhase { font-size: 12px; }
431
+ #loadingPercent { font-size: 11px; color: var(--fg-faint); }
432
+ /* The reload flavour of the same progress (see beginProgress in
433
+ viewer.js): a hairline bar along the top edge plus a small chip,
434
+ over a viewport that keeps showing the geometry already loaded.
435
+ Restoring the camera across a reload is pointless if the view is
436
+ blacked out while it happens. pointer-events stay off so a drag
437
+ that starts under the chip still pans the canvas. */
438
+ #reloadProgress { position: absolute; top: 0; left: 0; right: 0; z-index: 26; pointer-events: none; }
439
+ #reloadProgress.hidden { display: none; }
440
+ #reloadBarTrack { height: 2px; background: var(--surface); }
441
+ #reloadBarFill { width: 0%; height: 100%; background: var(--accent); transition: width 0.15s ease-out; }
442
+ #reloadLabel { display: inline-block; margin: 6px 0 0 10px; padding: 3px 8px; font-family: monospace;
443
+ font-size: 11px; color: var(--accent-text); background: var(--chip-scrim);
444
+ border: 1px solid var(--accent-border); border-radius: 3px; }
445
+ /* Load failures. This is deliberately NOT part of the debug tools:
446
+ #ui above is display:none unless the debug command is run, so an
447
+ error written only there is invisible to a normal user. Empty means
448
+ no error, so setting/clearing the text is all it takes to show or
449
+ hide the panel. */
450
+ #loadError { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 520px; z-index: 35; padding: 16px 20px; background: var(--error-bg); border: 1px solid var(--error-border); border-radius: 5px; font-family: monospace; font-size: 12px; line-height: 1.55; color: var(--error-fg); white-space: pre-line; user-select: text; }
451
+ #loadError:empty { display: none; }
452
+ #debugPanel { position: absolute; bottom: 0; left: 0; width: 100%; max-height: 240px; z-index: 40; background: var(--scrim); border-top: 1px solid var(--border); display: flex; flex-direction: column; }
453
+ #debugPanel.hidden { display: none; }
454
+ #debugPanelHeader { display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; font-family: monospace; font-size: 11px; color: var(--fg-dim); border-bottom: 1px solid var(--border); }
455
+ #debugPanelHeader button { font-family: monospace; font-size: 10px; background: var(--surface); color: var(--surface-fg); border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px; cursor: pointer; }
456
+ /* Readout beside the log rather than above it: the panel is capped at
457
+ 240px tall, and stacking them would leave the log a few lines. */
458
+ #debugBody { display: flex; flex: 1; min-height: 0; }
459
+ #debugLog { flex: 1; overflow-y: auto; padding: 6px 8px; font-family: monospace; font-size: 10px; color: var(--log-fg); white-space: pre-wrap; word-break: break-all; user-select: text; }
460
+ #debugLog .err { color: var(--log-err); }
461
+ #debugToggleBtn { position: absolute; bottom: 8px; left: 8px; z-index: 41; font-family: monospace; font-size: 10px; background: var(--surface); color: var(--fg-dim); border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px; cursor: pointer; opacity: 0.6; }
462
+ #debugToggleBtn:hover { opacity: 1; }
463
+
464
+ /* The progress bars are the only motion in the viewer. Someone who has
465
+ asked the OS for less of it still needs the bar to show progress --
466
+ so it still moves, it just stops easing there. */
467
+ @media (prefers-reduced-motion: reduce) {
468
+ #loadingBarFill, #reloadBarFill { transition: none; }
469
+ }
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="Content-Security-Policy"
6
+ content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self' 'wasm-unsafe-eval'; worker-src blob:; connect-src 'self';">
7
+ <title>GDS Lens</title>
8
+ <style>
9
+ /* The page harness only. Everything the viewer itself draws is styled
10
+ inside its shadow root (see viewer.css), which is why there is so
11
+ little here: a host page cannot reach in, and this cannot leak out. */
12
+ html, body { margin: 0; padding: 0; height: 100%; background: #1a1a1a; }
13
+ gds-lens { display: block; width: 100%; height: 100%; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <gds-lens></gds-lens>
18
+
19
+ <!-- Emscripten's output, loaded as a classic script for the
20
+ createGdstkModule global it defines. -->
21
+ <script src="gds-lens-engine.js"></script>
22
+ <!-- The embedder's ViewerHost. Must precede gds-lens.js, which reads
23
+ window.gdsLensHost as it starts. Defaults to the plain-page host; a
24
+ host that needs different services replaces this file, or sets
25
+ window.gdsLensHost itself and omits it. -->
26
+ <script src="gds-lens-host.js"></script>
27
+ <script src="gds-lens.js"></script>
28
+ </body>
29
+ </html>