gds-lens 0.1.1 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -4,10 +4,103 @@ Notable changes to this project. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- While the version is `0.x` the public API may change in any release. See the
8
- status note at the top of the README.
7
+ From 1.0.0 on, a breaking change to the element's API waits for a major
8
+ version. Before that, `0.x` releases changed it freely.
9
9
 
10
- ## [Unreleased]
10
+ ## [1.0.0] - 2026-08-25
11
+
12
+ ### Added
13
+
14
+ - Several `<gds-lens>` elements can now be live on one page. Each drives its
15
+ own viewer, with its own WebAssembly instance and WebGL2 context, so a `.lyp`
16
+ or marker database applied to one leaves the others alone. A second element
17
+ used to refuse visibly rather than contend for the renderer's state.
18
+ - `destroy()` on the element, releasing a viewer's WebAssembly instance and GL
19
+ context for good. Rarely needed -- an ordinary unmount parks the viewer
20
+ instead, which is what makes a framework remount free.
21
+ - `docs/react.md`: a wrapper component, the JSX type declaration for React 18
22
+ and 19, server rendering, and what remounting does. The examples are covered
23
+ by `test/react.test.js`, and the type declaration by
24
+ `test/types/jsx-smoke.tsx`.
25
+ - `showLoading(label?)` on the viewer surface, for the wait before `load()`:
26
+ a host that is fetching bytes can say so instead of leaving the viewer
27
+ looking idle. The element calls it itself when `load()` is given a URL.
28
+ - `examples/multi-view.html`: six viewers on one page -- three loading a layout
29
+ from `src`, three waiting for a button -- including applying a `.lyp` and a
30
+ marker database to one viewer alone, and creating and releasing a viewer to
31
+ keep a WebGL context free.
32
+ - `docs/embedding.md`, which is where the `ViewerHost` interface, the three
33
+ builds, the subpath exports and the WebAssembly limits moved to. The README
34
+ keeps the quick start and the element's own API and is a third of its previous
35
+ length.
36
+
37
+ ### Fixed
38
+
39
+ - Saved views were shared by every viewer on the page. Each read the whole set
40
+ at mount and wrote the whole set back on save, so two viewers that both saved
41
+ a view overwrote each other and the last one won. The default host now keeps a
42
+ bucket per viewer, keyed by the element's `id`, else its `src`, else -- for a
43
+ page with a single viewer and neither -- the key it has always used. `loadViews`
44
+ and `saveViews` are handed the viewer asking, which is what a host serving
45
+ several needs to tell them apart. A viewer with no `id` and no `src` on a page
46
+ with others has nothing stable to key on: its views last for the life of the
47
+ page. Views previously saved on a page whose element carries a `src` are not
48
+ carried into the new per-layout bucket.
49
+ - The renderer sized its drawing buffer to the window rather than to the canvas
50
+ element, which was only ever right for a viewer filling the page. In an
51
+ embedded `<gds-lens>` the browser stretched a window-sized buffer over the
52
+ element's box, so the layout was drawn distorted; every coordinate the mouse
53
+ produced -- the readout, the ruler, the right-click "Copy coordinate",
54
+ zoom-at-cursor -- answered for a pixel the pointer was not on; and each viewer
55
+ allocated a window-sized buffer and mask texture however small it was on
56
+ screen. It now sizes from the element, and a `ResizeObserver` on the canvas
57
+ keeps it in step with a box that changes without the window changing.
58
+ - The canvas right-click menu passed viewport coordinates to the renderer and
59
+ clamped itself against the window, so in an embedded viewer it opened in the
60
+ wrong place and reported the coordinate of a different pixel.
61
+ - A viewer that had not been asked for a layout showed "Loading layout..." over
62
+ an empty progress bar, which read as a load that had hung. It now says "No
63
+ layout loaded" until something actually asks for one.
64
+ - `import "gds-lens"` shipped no default host. `sideEffects` in package.json
65
+ did not list `src/hosts/browser.js`, whose whole purpose is the side effect
66
+ of installing `window.gdsLensHost`, so every bundler dropped it -- including
67
+ the one that builds `dist/esm`. The documented entry point therefore came up
68
+ with no `.lyp` or marker pickers, no saved views, no drag-and-drop, no
69
+ `window.gdsLens`, and a console error claiming no layout would ever appear on
70
+ pages whose layout had loaded perfectly well. The served payloads were never
71
+ affected, since they load `gds-lens-host.js` as a separate script.
72
+
73
+ The `esm-bundle` test that was meant to catch this asserted on the string
74
+ `gdsLensHost`, which viewer.js contains anyway; it now looks for the host's
75
+ own implementation.
76
+ - A viewer adopted while its WebAssembly module was still starting failed to
77
+ come up at all, reporting `Cannot set properties of null (setting 'width')`.
78
+ `ready` resolves as soon as the viewer is built, which is well before `main()`
79
+ creates the GL context, so an element could be removed and its viewer adopted
80
+ inside that window -- and `adopt` re-pointed the module's DOM root on the next
81
+ turn of the module promise, which is after `main()` has already read it. It
82
+ now writes through the object Emscripten uses *as* the Module, so the new root
83
+ is in place immediately.
84
+ - An element whose parked viewer had been adopted by another element went on
85
+ driving it, so a stray `load()` or `src` change on a detached element wrote
86
+ into whatever was on screen. Such an element now rejects instead.
87
+ - Removing a `<gds-lens>` before its engine finished loading raised an unhandled
88
+ promise rejection, which reached the embedding app's error reporting.
89
+ - An element removed and re-added before its engine arrived built two viewers,
90
+ orphaning the first one's WebAssembly instance and WebGL context behind the
91
+ second's shadow tree.
92
+ - The viewer no longer assigns `window.onerror`, which clobbered the embedding
93
+ page's handler. Page-level failures are reported through an added listener
94
+ and fanned out to whichever debug panels are open.
95
+
96
+ ### Changed
97
+
98
+ - `src/viewer.js` exports `createViewer(element)` instead of doing its work in
99
+ its module body, which is what allows more than one viewer. `window.gdsLensHost`
100
+ is consequently read when each viewer mounts rather than once at import, so an
101
+ app can install a host any time before its first `<gds-lens>` renders.
102
+ - `src/mount-target.js` is gone. It existed only to hand an element to
103
+ viewer.js's module body, which is now a parameter.
11
104
 
12
105
  ## [0.1.1] - 2026-08-25
13
106
 
@@ -194,6 +287,7 @@ web page rather than for a webview.
194
287
  worker-loading route and shipped unsubstituted. The `createWorker` host hook
195
288
  replaces it.
196
289
 
197
- [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.1...HEAD
290
+ [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.0.0...HEAD
291
+ [1.0.0]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.1...v1.0.0
198
292
  [0.1.1]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.0...v0.1.1
199
293
  [0.1.0]: https://github.com/EthanLowenthal/GDS-Lens/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # GDS Lens
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/gds-lens.svg?style=flat-square&color=0f1720&label=NPM)](https://www.npmjs.com/package/gds-lens)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENCE.md)
5
+
3
6
  GDSII and OASIS chip layouts, parsed and rendered in the browser. A drop-in
4
7
  custom element compiled to pure JS+WASM with a WebGL2 renderer.
5
8
 
6
9
  ![The viewer: a cell hierarchy tree, a rendered photonic layout, and the layer and display controls](https://raw.githubusercontent.com/EthanLowenthal/GDS-Lens/main/images/example.png)
7
10
 
8
- > **Status**: pre-1.0. The API is unstable and will change.
11
+ > **Status**: 1.0. The element's API is stable, and breaking changes wait
12
+ > for a major version. See [the changelog](CHANGELOG.md).
9
13
 
10
14
  ## Quick start
11
15
 
@@ -39,41 +43,9 @@ await viewer.load("chip.gds"); // a URL, or bytes you already have
39
43
  await viewer.goToPoint(120.5, -40); // center on a coordinate, in microns
40
44
  ```
41
45
 
42
- ### Serve the files instead of bundling
43
-
44
- If you would rather serve a payload than bundle one — for a smaller download
45
- and a streaming WebAssembly compile — copy `dist/web/` and load its scripts in
46
- order. The order matters: both must precede `gds-lens.js`, which reads them as
47
- it starts.
48
-
49
- ```html
50
- <script src="gds-lens-engine.js"></script> <!-- the wasm module -->
51
- <script src="gds-lens-host.js"></script> <!-- the default ViewerHost -->
52
- <script src="gds-lens.js"></script> <!-- the element -->
53
-
54
- <gds-lens src="chip.gds" style="width: 100%; height: 600px"></gds-lens>
55
- ```
56
-
57
- #### What each file is
58
-
59
- The payload is six files, four of which are required:
60
-
61
- | File | Status | What it is |
62
- |---|---|---|
63
- | `gds-lens.js` | required | The element, the viewer, and the control panel. This is the package. Load it last. |
64
- | `gds-lens-engine.js` | required | gdstk's GDSII and OASIS reader and the WebGL2 renderer, compiled to WebAssembly. Defines the `createGdstkModule` global `gds-lens.js` looks for, which is why it goes first. The parse worker loads it too. |
65
- | `gds-lens-engine.wasm` | required | The binary, fetched by `gds-lens-engine.js` from beside it. `inline-wasm` embeds it instead. |
66
- | `gds-lens-worker.js` | required | The parse worker: reads and triangulates off the main thread so the canvas stays responsive. Fetched by the worker, never by your page. |
67
- | `gds-lens-host.js` | optional | The default `ViewerHost`, described in [The ViewerHost interface](#the-viewerhost-interface). If you set `window.gdsLensHost` yourself before `gds-lens.js` runs, omit it. |
68
- | `gds-lens.html` | optional | A working reference page. For the script order, read this file; there is no need to deploy it. |
69
-
70
- Every name carries the package prefix because these get copied into someone
71
- else's web root, where a bare `host.js` or `wasm-worker.js` is a likely
72
- collision. Serve them from one directory, in the preceding order.
73
-
74
- `dist/web/gds-lens.html` is a working page doing exactly this. Everything in the
75
- payload must be on the same origin as the page: the WebAssembly binary and the
76
- parse worker are both fetched relative to the scripts.
46
+ If you would rather serve a payload than bundle one — for a smaller download and
47
+ a streaming WebAssembly compile — see
48
+ [Embedding GDS Lens](docs/embedding.md#serve-the-files-instead-of-bundling).
77
49
 
78
50
  ## What it does
79
51
 
@@ -100,15 +72,18 @@ CI rather than committed.
100
72
 
101
73
  ## Reference
102
74
 
103
- The following sections describe the element, the `ViewerHost` interface an
104
- embedder implements, the package's subpath exports, and the limits of the
105
- WebAssembly module.
75
+ The following sections describe the element's own API. The rest lives beside
76
+ it:
77
+
78
+ | Page | What is in it |
79
+ |---|---|
80
+ | [React integration](docs/react.md) | A wrapper component, the JSX type declaration, server rendering, remounts. |
81
+ | [Embed the viewer](docs/embedding.md) | The `ViewerHost` interface, the three builds, the subpath exports, and the limits of the WebAssembly module. |
106
82
 
107
83
  ### The `<gds-lens>` element
108
84
 
109
85
  The element is `display: block` with no intrinsic height, so give it one. It
110
- takes one attribute, exposes a handful of methods, and only one instance can be
111
- live at a time.
86
+ takes one attribute and exposes a handful of methods.
112
87
 
113
88
  #### Attributes
114
89
 
@@ -130,188 +105,47 @@ The element exposes the following members:
130
105
  | `setLyp(name, text)` | `Promise<void>` | Applies a `.lyp` layer-properties file. Pass `""` to clear. |
131
106
  | `setMarkers(name, text)` | `Promise<void>` | Applies a marker database. The viewer detects the format from the content. |
132
107
  | `showError(message)` | `Promise<void>` | Replaces the view with an error message. |
108
+ | `destroy()` | `Promise<void>` | Releases the viewer's WebAssembly instance and WebGL context for good. Rarely needed — see [Removal parks the viewer](#removal-parks-the-viewer). |
133
109
 
134
- #### Only one live element at a time
110
+ #### Several viewers on one page
135
111
 
136
- The renderer keeps its state in module-scope globals, so only one `<gds-lens>`
137
- can be *live* at a time. A second one alongside the first refuses visibly
138
- rather than contending for the same WebGL context.
112
+ Each `<gds-lens>` drives its own viewer, with its own shadow tree, its own
113
+ WebAssembly instance, and its own WebGL2 context. Put as many on a page as you
114
+ like; they share nothing, so a `.lyp` or a marker database applied to one leaves
115
+ the others alone.
139
116
 
140
- Removing one and adding another is fine, though, which is what matters in a
141
- framework: React and friends recreate the node on re-render, and an SPA route
142
- change destroys and rebuilds it. The engine is not torn down when the element
143
- leaves the DOM — the next `<gds-lens>` to connect has it moved into it, keeping
144
- the WebGL context, the parsed design, and the camera. Nothing reloads.
117
+ Each one costs a WebGL2 context, though, and browsers cap live contexts per page
118
+ at roughly 8 to 16 — past that the browser starts dropping the oldest. A page
119
+ showing a dozen layouts at once wants one viewer swapping layouts rather than a
120
+ dozen elements.
145
121
 
146
- ### The `ViewerHost` interface
122
+ [`examples/multi-view.html`](examples/multi-view.html) is a working page of six:
123
+ three loading a layout from `src`, three waiting for a button, one of them
124
+ created and released on demand to keep a context free.
147
125
 
148
- The viewer never touches the environment directly. Anything only an embedder
149
- can do goes through a host object, which you install as `window.gdsLensHost`
150
- before the element script runs. Leave it unset and a default host handles a
151
- plain web page: `<input type=file>` for the pickers, `localStorage` for saved
152
- views, `prompt()` for a name, plus `?src=` and drag-and-drop for loading.
126
+ #### Removal parks the viewer
153
127
 
154
- Two things the default host does to the page, both of which a replacement host
155
- inherits responsibility for and neither of which the element itself does: it
156
- publishes the viewer surface as `window.gdsLens`, so a plain page can drive it
157
- from a script tag or the console; and it binds drag-and-drop to the
158
- `<gds-lens>` element rather than to `window`, which leaves the rest of the
159
- page's own drop targets alone.
128
+ An element leaving the DOM *parks* its viewer rather than tearing it down, and
129
+ the next `<gds-lens>` to mount without one of its own adopts it. That is what
130
+ makes a framework remount free: the element is new, the viewer is not, and the
131
+ parsed design, the camera and the GL context all survive.
160
132
 
161
- Every method is optional. The viewer hides the control for anything a host
162
- does not implement, so a read-only embed can supply almost none of it. A host
163
- can implement any of the following methods:
133
+ An element mounted *alongside* a live one finds nothing parked and builds its
134
+ own, which is why the preceding section is true.
164
135
 
165
- | Method | Returns | Called when |
166
- |---|---|---|
167
- | `pickLyp()` | `Promise<{name, text} \| null>` | The user asks for a `.lyp`. `null` means cancelled. |
168
- | `unloadLyp()` | `void` | The loaded `.lyp` is dismissed. |
169
- | `pickMarkers()` | `Promise<{name, text} \| null>` | The user asks for a marker database. |
170
- | `unloadMarkers()` | `void` | The loaded marker database is dismissed. |
171
- | `loadViews()` | `Promise<View[]>` | Once at mount, for saved camera positions. |
172
- | `saveViews(views)` | `void` | The saved-view set changed. Persist it. |
173
- | `promptViewName(existing)` | `Promise<string \| null>` | A view is being saved. `existing` is the names already used. |
174
- | `requestReload()` | `void` | The user asks to re-read the layout. |
175
- | `setAutoReload(on)` | `void` | The user asks to always reload on change. |
176
- | `onGotoResult({ok, x, y})` | `void` | A `goToPoint` call finished, reporting whether it landed inside. |
177
- | `isLightTheme()` | `boolean` | The viewer needs to know the theme. Defaults to the OS preference. |
178
- | `createWorker()` | `Worker` | The parse Worker is needed. Where the scripts cannot be fetched by URL, override this. |
179
- | `connect(viewer)` | `void` | At mount, handing you the surface described in the following section. |
180
-
181
- #### The viewer surface
182
-
183
- `connect(viewer)` gives you the other direction, for pushing into the viewer
184
- rather than answering it:
185
-
186
- | Method | Description |
187
- |---|---|
188
- | `load(bytes, {reload})` | Display a layout from bytes. |
189
- | `showError(message)` | Show a fatal error. |
190
- | `setLyp(name, text)` | Apply layer properties. |
191
- | `setMarkers(name, text)` | Apply a marker database. |
192
- | `showStale(text)` | Offer a reload, for when the file changed underneath. |
193
- | `goToPoint(x, y)` | Center on a coordinate. |
194
- | `toggleDebug()` | Show or hide the debug panel. |
195
- | `setNamedViews(views)` | Replace the saved-view set. |
196
- | `applyTheme()` | Re-ask `isLightTheme()` after a theme change. |
197
- | `element` | The `<gds-lens>` the viewer is mounted in. Bind anything of your own to this rather than to `window`, so it stays inside the component. |
198
-
199
- #### Example: a custom host
200
-
201
- ```js
202
- window.gdsLensHost = {
203
- async pickLyp() {
204
- const text = await fetch("/pdk/layers.lyp").then((r) => r.text());
205
- return { name: "layers.lyp", text };
206
- },
207
- isLightTheme: () => document.documentElement.dataset.theme === "light",
208
- connect(viewer) {
209
- myApp.on("layout", (bytes) => viewer.load(bytes));
210
- }
211
- };
212
- ```
213
-
214
- ### Subpath exports
215
-
216
- The parsers are pure JavaScript with no DOM and no WebAssembly, so they import
217
- cleanly into Node, a worker, or an extension host. The package exposes the
218
- following entry points:
219
-
220
- | Import | Exports |
221
- |---|---|
222
- | `gds-lens` | `GdsLens`, and registers `<gds-lens>`. The bundled module — everything inlined |
223
- | `gds-lens/parsers` | `parseMarkerFile`, `parseLyrdb`, `parseDrcAscii`, `sniffMarkerFormat`, `parsePointList`, `flattenMarkerModel` |
224
- | `gds-lens/layout-bytes` | `decodeLayoutBytes`, `looksGzipped`, `gzipStoredSize` |
225
- | `gds-lens/coord-parse` | `parseCoordinatePair` |
226
- | `gds-lens/cell-search` | `rankCellMatches`, `cellPathToTarget` |
227
- | `gds-lens/load-errors` | `describeLoadFailure`, `isOutOfMemory`, `describeDecodeFailure` |
228
- | `gds-lens/hosts/browser` | `createBrowserHost` |
229
- | `gds-lens/web/*` | The prebuilt payload, for hosts that serve files rather than bundle |
230
- | `gds-lens/inline-wasm/*` | The same payload with the binary embedded, for hosts that cannot fetch |
231
- | `gds-lens/esm/*` | The bundled module by path, if you would rather not rely on `.` |
232
-
233
- TypeScript declarations ship for all of these.
234
-
235
- #### The three builds
236
-
237
- `import "gds-lens"` gets you `dist/esm/`, and that is the right default. The
238
- other two are there for cases it cannot cover. The following table compares
239
- them:
240
-
241
- | Property | `esm` | `web` | `inline-wasm` |
242
- |---|---|---|---|
243
- | How it arrives | `import "gds-lens"` | scripts you serve | scripts you serve |
244
- | Files to serve | none | 4 (+2 optional) | 3 (+2 optional) |
245
- | Bundler configuration | none | N/A | N/A |
246
- | Total transfer, gzipped | 252 KB | 235 KB | 243 KB |
247
- | Streaming WebAssembly compile | no | yes | no |
248
- | Binary cached separately from the JS | no | yes | no |
249
- | Sensitive to the page's encoding | no | no | yes |
250
- | Needs `blob:` in `script-src` | yes | no | no |
251
-
252
- **`esm`** is one file with everything inside it: the markup, the styles,
253
- lil-gui, the default host, the WebAssembly binary, and the parse worker's whole
254
- script. Nothing is fetched, so nothing has to be served or copied, and no
255
- bundler needs configuring. It costs the streaming compile and about 17 KB over
256
- the payload.
257
-
258
- Both the main thread and the parse worker need Emscripten's module, and a
259
- worker cannot share the main thread's copy. Rather than inline it twice —
260
- which would add about 190 KB gzipped for nothing — it is inlined once as text
261
- and loaded from a `blob:` URL by both. That is the one thing this build asks
262
- of a page's CSP that the others do not: `blob:` in `script-src`, on top of
263
- the `worker-src blob:` all three need.
264
-
265
- **`web`** is the payload to serve if you can. `gds-lens-engine.js` fetches
266
- `gds-lens-engine.wasm` from beside it, so serve the two together; the binary
267
- compiles as it streams and is cached on its own.
268
-
269
- **`inline-wasm`** is `web` with the binary embedded in `gds-lens-engine.js`, for
270
- hosts that cannot fetch a file next to their own scripts — a VS Code webview
271
- cannot, from a Worker or from the main thread. Nothing else differs.
272
-
273
- On that last row: `inline-wasm` embeds the binary as a raw string, so
274
- `gds-lens-engine.js` has to be *decoded* as UTF-8 or it is corrupted, and the module
275
- then fails with a `WebAssembly.instantiate()` error about section lengths that
276
- says nothing about the cause. Either `Content-Type: text/javascript;
277
- charset=utf-8` or `<meta charset="UTF-8">` on the page satisfies it; only the
278
- absence of both breaks. That payload warns in the console when the document is
279
- not UTF-8. `esm` escapes its non-ASCII, so it does not care.
280
-
281
- #### Compressed layouts
282
-
283
- Gzip is handled for you: `<gds-lens src="chip.gds.gz">` works, and so does a
284
- plain `.gds` that is secretly gzipped, because the format is decided by magic
285
- number rather than by filename. Expansion happens in JavaScript rather than
286
- inside the WebAssembly heap, which is where a second copy of the
287
- file is most expensive, and it is capped at 2 GB.
288
-
289
- The same decoder is exported if you want it separately — to check a file before
290
- handing it over, say:
291
-
292
- ```js
293
- import { decodeLayoutBytes } from "gds-lens/layout-bytes";
294
-
295
- const result = await decodeLayoutBytes(new Uint8Array(await file.arrayBuffer()));
296
- if (result.ok) await viewer.load(result.bytes);
297
- else console.error(result.reason); // "too-large" | "corrupt"
298
- ```
136
+ Nothing frees a parked viewer on its own. A page that creates viewers it will
137
+ never show again, and is hitting the context limit, can call `destroy()`; an
138
+ ordinary unmount should let it park. See
139
+ [Remounts and StrictMode](docs/react.md#remounts-and-strictmode) for what this
140
+ means in a framework.
299
141
 
300
142
  ### Limits
301
143
 
302
- Parsing, flattening, and triangulating all happen inside a 32-bit WebAssembly
303
- module, so everything has to fit in one 4 GB address space. Two cases bound
304
- what fits:
305
-
306
- - **Flat geometry is the expensive case**. It costs roughly 1 KB per polygon
307
- end to end, so a couple of million top-level polygons is the practical
308
- ceiling.
309
- - **Hierarchy is nearly free**. A cell placed eight or more times becomes a GPU
310
- instance batch: 24 bytes per placement rather than a full geometry copy. A
311
- design that flattens to 115 million polygons loads in about 2 GB.
312
-
313
- Past that the module aborts, and the viewer turns the error into an explanation
314
- rather than an engine string.
144
+ The parser and renderer run in a 32-bit WebAssembly address space, so a layout
145
+ has to fit file plus geometry inside it. See
146
+ [Limits](docs/embedding.md#limits) for the numbers and
147
+ [Compressed layouts](docs/embedding.md#compressed-layouts) for what that means
148
+ for gzipped files.
315
149
 
316
150
  ## Build from source
317
151