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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,167 @@
1
+ # Changelog
2
+
3
+ Notable changes to this project. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
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.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0] - 2026-08-25
13
+
14
+ First release as a library.
15
+
16
+ GDS Lens was previously a VS Code extension, released under the tags `v1.0.0`
17
+ through `v1.6.3` and never published to npm. Those tags remain in the
18
+ repository as history; this is a different thing with a different API, so the
19
+ version starts again rather than continuing from `1.6.3`. The extension host
20
+ has been replaced by the `ViewerHost` interface, the viewer mounts as a
21
+ `<gds-lens>` custom element in a shadow root, and the payload is built for any
22
+ web page rather than for a webview.
23
+
24
+ ### Added
25
+
26
+ - `<gds-lens>` custom element: `src`, `load`, `goToPoint`, `setLyp`,
27
+ `setMarkers`, `showError`, `ready`.
28
+ - **`dist/esm/`, a single importable module.** `import "gds-lens"` now resolves
29
+ to one file with the markup, styles, lil-gui, the default host, the
30
+ WebAssembly binary and the parse worker's script all inside it -- no bundler
31
+ configuration and no sibling files to serve. Built from a new
32
+ `GDS_LENS_ESM` wasm variant (`-sEXPORT_ES6`). The binary is inlined once and
33
+ shared with the worker through a `blob:` URL rather than inlined twice, which
34
+ is why this build needs `blob:` in `script-src`; 252 KB gzipped. The module
35
+ imports without a DOM, so a server render of a page that uses the element
36
+ does not throw -- the element registers itself on the client, where
37
+ `customElements` exists.
38
+ - `describeDecodeFailure` on `gds-lens/load-errors`, and `limit` on the failure
39
+ result from `decodeLayoutBytes`.
40
+ - The `ViewerHost` interface, with every method optional -- the viewer removes
41
+ the control for anything a host does not implement. A default host handles a
42
+ plain page.
43
+ - Hand-written TypeScript declarations for the element, the host contract and
44
+ every pure subpath export.
45
+ - Subpath exports for the parsers, which have no DOM and no WebAssembly:
46
+ `gds-lens/parsers`, `/cell-search`, `/coord-parse`, `/layout-bytes`,
47
+ `/load-errors`, `/hosts/browser`.
48
+ - Two prebuilt payloads, `gds-lens/web/*` and `gds-lens/inline-wasm/*`,
49
+ differing only in whether the wasm binary is a separate file.
50
+ - `debug` attribute and `?gdsDebug=1` for trace output.
51
+ - Continuous integration: lint and the pure tests on every push, plus a lane
52
+ that builds both wasm payloads and runs the browser suite against them.
53
+ Publishing runs from a `v*` tag in the same way -- built and tested in the
54
+ job that publishes it, so the tarball on the registry is one a green run
55
+ produced rather than whatever a laptop had on disk. It authenticates with
56
+ npm trusted publishing rather than a stored token, which also means the
57
+ published tarball carries provenance: a signed statement of the commit and
58
+ workflow that built it, verifiable with `npm audit signatures`.
59
+
60
+ ### Changed
61
+
62
+ - **Every file in the served payloads carries the package prefix.** They get
63
+ copied into someone else's web root, where `host.js`, `wasm-worker.js` and
64
+ `gdstk_wasm.js` are collisions waiting to happen:
65
+
66
+ | was | is |
67
+ |---|---|
68
+ | `host.js` | `gds-lens-host.js` |
69
+ | `wasm-worker.js` | `gds-lens-worker.js` |
70
+ | `gdstk_wasm.js` | `gds-lens-engine.js` |
71
+ | `gdstk_wasm.wasm` | `gds-lens-engine.wasm` |
72
+ | `viewer.html` | `gds-lens.html` |
73
+
74
+ The `createGdstkModule` global is unchanged: it is distinctive enough not to
75
+ clash, and it says what the module actually is.
76
+ - Lint runs `eslint:recommended` as errors rather than seven rules as
77
+ warnings, so it can actually fail.
78
+ - `prepublishOnly` gates a publish on lint, tests and two checks:
79
+ `scripts/check-dist.mjs`, which refuses a `dist/` that is missing, older than
80
+ the sources it was built from, or still carrying a template placeholder; and
81
+ `scripts/check-package.mjs`, which reads the file list npm would actually
82
+ publish and refuses build output, object files, CMake artifacts, the wasm
83
+ sources, or anything containing the build machine's home directory.
84
+ - Third-party notices now cover the whole payload, not only what is linked
85
+ into the WebAssembly: zlib and Emscripten (in `gdstk_wasm.js`) and lil-gui
86
+ (bundled into `gds-lens.js`) have been added.
87
+
88
+ ### Fixed
89
+
90
+ - **`import "gds-lens"` works.** The main entry pointed at `src/gds-lens.js`,
91
+ which pulls in `viewer.js` -- needing `.html`/`.css` text imports, a
92
+ `lil-gui-css` alias that existed only inside this repo's build, and the wasm
93
+ factory already present as a global. Nothing could load it: not Node, not any
94
+ bundler. It now resolves to the bundled module above.
95
+ - **Gzipped layouts open.** `decodeLayoutBytes` was fully implemented and unit
96
+ tested but had no caller anywhere in `src/`, so `.gds.gz` and `.oas.gz`
97
+ failed with "Could not open this layout" despite the README advertising
98
+ them. It is wired into the load path, which every entry point goes through.
99
+ Bytes handed in as an `ArrayBuffer` are normalized too, so a compressed file
100
+ arriving in that shape is detected rather than sailing past the sniff.
101
+ - **A removed `<gds-lens>` can be replaced.** The one-per-page guard was never
102
+ cleared, so after the first element left the DOM every later one refused
103
+ permanently -- which is what a framework re-render or an SPA route change
104
+ does. The element now releases its claim on disconnect and the next one has
105
+ the running engine moved into it, keeping the WebGL context, the parsed
106
+ design and the camera.
107
+ - **An error message is no longer treated as markup.** A load failure's text
108
+ can carry a filename, a gdstk string, or -- through the default host's
109
+ `?src=` handling -- text straight from the URL, and it was concatenated into
110
+ `innerHTML` for the debug readout. A crafted link could inject into the page.
111
+ It is built as a text node now. Pages using the payload's own `viewer.html`
112
+ were protected by its CSP; an embedder with a laxer policy was not.
113
+ - **The host page's `console` is no longer replaced.** The viewer overwrote
114
+ `console.log` and `console.error` to feed its on-screen debug panel, so a
115
+ host application's own logging appended to a detached `<div>` for the life of
116
+ the page. Trace output now goes through an internal logger and reaches the
117
+ console only when asked for.
118
+ - **Drag-and-drop no longer covers the whole page.** The default host bound
119
+ `dragover`/`drop` to `window` and called `preventDefault`, which silently
120
+ disabled the embedding application's own drop targets. Both are bound to the
121
+ `<gds-lens>` element.
122
+ - **Neither the Emscripten build tree nor the C++ ships.** `files` in
123
+ `package.json` is an allowlist that overrides `.gitignore`, so listing `src/`
124
+ published the whole build directory: 211 object files and CMake caches,
125
+ 2.0 MB, with absolute paths from the build machine inside them. The wasm
126
+ sources went with them, which is no better a use of an install -- a consumer
127
+ gets the compiled payload and has no toolchain to rebuild it from
128
+ `renderer.cpp`. A `!src/wasm` negation excludes both, and `check:package`
129
+ fails on either coming back. The tarball is 39 files, 812 KB packed.
130
+ - **Absolute build paths no longer end up in the WebAssembly.** With no
131
+ `CMAKE_BUILD_TYPE`, `NDEBUG` was never defined, so `assert()` stayed live in
132
+ gdstk, earcut and libcxxabi -- each embedding `__FILE__` as the full path of
133
+ the machine that compiled it, six of which shipped inside `gdstk_wasm.wasm`
134
+ and the inline payload. `-ffile-prefix-map` rewrites the prefix, which also
135
+ makes the binary reproducible across checkouts. The asserts are deliberately
136
+ kept: this module parses untrusted layouts, and an assert turns a violated
137
+ invariant into a clean abort the viewer already explains.
138
+ - Saved views coming back from `localStorage` are checked for being an array,
139
+ not merely for parsing.
140
+ - The debug panel drops old lines past a cap instead of growing without bound.
141
+
142
+ ### Accessibility
143
+
144
+ - Controls that were `<span>`s with click handlers are real buttons, so they
145
+ can be reached and operated from the keyboard.
146
+ - The canvas is focusable and named; the hierarchy is a proper `tree` with
147
+ per-row expanded state and depth.
148
+ - Load progress, load errors and the stale-file banner are announced;
149
+ disclosures report `aria-expanded` and the search scope pair
150
+ `aria-pressed`.
151
+ - The progress bars respect `prefers-reduced-motion`.
152
+
153
+ ### Removed
154
+
155
+ - The `gds-lens/viewer` export. It pointed at `src/viewer.js`, which cannot be
156
+ imported by anything -- it needs the wasm factory as a pre-loaded global and
157
+ text imports for its markup and styles -- so it was a promise nothing could
158
+ keep. The same surface is reachable through `element.ready`.
159
+ - `bindings.cpp` and its `parseGds` export, a second parse path with no
160
+ caller, compiled into every build.
161
+ - The `loadAndRenderGds` export, also unused.
162
+ - The `#workerBundle` placeholder element, left over from the extension's
163
+ worker-loading route and shipped unsubstituted. The `createWorker` host hook
164
+ replaces it.
165
+
166
+ [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.0...HEAD
167
+ [0.1.0]: https://github.com/EthanLowenthal/GDS-Lens/releases/tag/v0.1.0
package/LICENCE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ethan Lowenthal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # GDS Lens
2
+
3
+ GDSII and OASIS chip layouts, parsed and rendered in the browser. A drop-in
4
+ custom element wrapping [gdstk](https://github.com/heitzmann/gdstk) compiled to
5
+ WebAssembly and a WebGL2 renderer.
6
+
7
+ Reading and rendering only. Writing layouts is deliberately out of scope.
8
+
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)
10
+
11
+ > **Status**: pre-1.0. The API is unstable and will change.
12
+
13
+ ## Quick start
14
+
15
+ ```sh
16
+ npm install gds-lens
17
+ ```
18
+
19
+ ```js
20
+ import "gds-lens"; // registers <gds-lens>
21
+ ```
22
+
23
+ ```html
24
+ <gds-lens src="chip.gds" style="width: 100%; height: 600px"></gds-lens>
25
+ ```
26
+
27
+ That's the whole thing. Pan with the mouse, zoom with the wheel. The import
28
+ pulls in one self-contained module — the parser, the renderer, the WebAssembly
29
+ binary, and the control panel — so there is nothing to copy and nothing
30
+ else to serve.
31
+
32
+ Or drive it from JavaScript:
33
+
34
+ ```js
35
+ import "gds-lens";
36
+
37
+ const viewer = document.createElement("gds-lens");
38
+ viewer.style.cssText = "width: 100%; height: 600px";
39
+ document.body.append(viewer);
40
+
41
+ await viewer.load("chip.gds"); // a URL, or bytes you already have
42
+ await viewer.goToPoint(120.5, -40); // center on a coordinate, in microns
43
+ ```
44
+
45
+ ### Serve the files instead of bundling
46
+
47
+ If you would rather serve a payload than bundle one — for a smaller download
48
+ and a streaming WebAssembly compile — copy `dist/web/` and load its scripts in
49
+ order. The order matters: both must precede `gds-lens.js`, which reads them as
50
+ it starts.
51
+
52
+ ```html
53
+ <script src="gds-lens-engine.js"></script> <!-- the wasm module -->
54
+ <script src="gds-lens-host.js"></script> <!-- the default ViewerHost -->
55
+ <script src="gds-lens.js"></script> <!-- the element -->
56
+
57
+ <gds-lens src="chip.gds" style="width: 100%; height: 600px"></gds-lens>
58
+ ```
59
+
60
+ #### What each file is
61
+
62
+ The payload is six files, four of which are required:
63
+
64
+ | File | Status | What it is |
65
+ |---|---|---|
66
+ | `gds-lens.js` | required | The element, the viewer, and the control panel. This is the package. Load it last. |
67
+ | `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. |
68
+ | `gds-lens-engine.wasm` | required | The binary, fetched by `gds-lens-engine.js` from beside it. `inline-wasm` embeds it instead. |
69
+ | `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. |
70
+ | `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. |
71
+ | `gds-lens.html` | optional | A working reference page. For the script order, read this file; there is no need to deploy it. |
72
+
73
+ Every name carries the package prefix because these get copied into someone
74
+ else's web root, where a bare `host.js` or `wasm-worker.js` is a likely
75
+ collision. Serve them from one directory, in the preceding order.
76
+
77
+ `dist/web/gds-lens.html` is a working page doing exactly this. Everything in the
78
+ payload must be on the same origin as the page: the WebAssembly binary and the
79
+ parse worker are both fetched relative to the scripts.
80
+
81
+ ## What it does
82
+
83
+ - **Parses GDSII and OASIS**. Format and gzip are both detected from the
84
+ leading bytes rather than the filename. The reader is gdstk itself, which is
85
+ what other implementations validate against.
86
+ - **Renders with WebGL2**. Layer-batched vertex buffers, GPU instancing for
87
+ repeated cells, and a stroke font all live in C++ compiled alongside the
88
+ parser.
89
+ - **Reads layer properties**. A `.lyp` file supplies colors, fill styles, and
90
+ layer names.
91
+ - **Browses DRC and LVS markers**. The viewer reads `.lyrdb` report databases
92
+ and ASCII DRC results.
93
+ - **Navigates hierarchy**. You can search cells and labels, and measure
94
+ distances.
95
+
96
+ ## Installation
97
+
98
+ The package ships prebuilt: no Emscripten toolchain is required to consume it.
99
+ Installing straight from a git URL does not work, because `dist/` is built in
100
+ CI rather than committed.
101
+
102
+ ---
103
+
104
+ ## Reference
105
+
106
+ The following sections describe the element, the `ViewerHost` interface an
107
+ embedder implements, the package's subpath exports, and the limits of the
108
+ WebAssembly module.
109
+
110
+ ### The `<gds-lens>` element
111
+
112
+ The element is `display: block` with no intrinsic height, so give it one. It
113
+ takes one attribute, exposes a handful of methods, and only one instance can be
114
+ live at a time.
115
+
116
+ #### Attributes
117
+
118
+ The element takes one attribute:
119
+
120
+ | Attribute | Description |
121
+ |---|---|
122
+ | `src` | URL of a layout to fetch and display. Setting it later reloads. |
123
+
124
+ #### Properties and methods
125
+
126
+ The element exposes the following members:
127
+
128
+ | Member | Returns | Description |
129
+ |---|---|---|
130
+ | `ready` | `Promise<Viewer>` | Resolves once the engine has mounted. Every method in the following table awaits this, so you rarely need it directly. |
131
+ | `load(source, options?)` | `Promise<void>` | `source` is a URL string, a `Uint8Array`, or an `ArrayBuffer`. `options.reload` keeps the current camera and layer visibility instead of framing the design. |
132
+ | `goToPoint(x, y)` | `Promise<boolean>` | Centers on a coordinate in microns and flashes a crosshair. Resolves `true` if the point is inside the layout. |
133
+ | `setLyp(name, text)` | `Promise<void>` | Applies a `.lyp` layer-properties file. Pass `""` to clear. |
134
+ | `setMarkers(name, text)` | `Promise<void>` | Applies a marker database. The viewer detects the format from the content. |
135
+ | `showError(message)` | `Promise<void>` | Replaces the view with an error message. |
136
+
137
+ #### Only one live element at a time
138
+
139
+ The renderer keeps its state in module-scope globals, so only one `<gds-lens>`
140
+ can be *live* at a time. A second one alongside the first refuses visibly
141
+ rather than contending for the same WebGL context.
142
+
143
+ Removing one and adding another is fine, though, which is what matters in a
144
+ framework: React and friends recreate the node on re-render, and an SPA route
145
+ change destroys and rebuilds it. The engine is not torn down when the element
146
+ leaves the DOM — the next `<gds-lens>` to connect has it moved into it, keeping
147
+ the WebGL context, the parsed design, and the camera. Nothing reloads.
148
+
149
+ ### The `ViewerHost` interface
150
+
151
+ The viewer never touches the environment directly. Anything only an embedder
152
+ can do goes through a host object, which you install as `window.gdsLensHost`
153
+ before the element script runs. Leave it unset and a default host handles a
154
+ plain web page: `<input type=file>` for the pickers, `localStorage` for saved
155
+ views, `prompt()` for a name, plus `?src=` and drag-and-drop for loading.
156
+
157
+ Two things the default host does to the page, both of which a replacement host
158
+ inherits responsibility for and neither of which the element itself does: it
159
+ publishes the viewer surface as `window.gdsLens`, so a plain page can drive it
160
+ from a script tag or the console; and it binds drag-and-drop to the
161
+ `<gds-lens>` element rather than to `window`, which leaves the rest of the
162
+ page's own drop targets alone.
163
+
164
+ Every method is optional. The viewer hides the control for anything a host
165
+ does not implement, so a read-only embed can supply almost none of it. A host
166
+ can implement any of the following methods:
167
+
168
+ | Method | Returns | Called when |
169
+ |---|---|---|
170
+ | `pickLyp()` | `Promise<{name, text} \| null>` | The user asks for a `.lyp`. `null` means cancelled. |
171
+ | `unloadLyp()` | `void` | The loaded `.lyp` is dismissed. |
172
+ | `pickMarkers()` | `Promise<{name, text} \| null>` | The user asks for a marker database. |
173
+ | `unloadMarkers()` | `void` | The loaded marker database is dismissed. |
174
+ | `loadViews()` | `Promise<View[]>` | Once at mount, for saved camera positions. |
175
+ | `saveViews(views)` | `void` | The saved-view set changed. Persist it. |
176
+ | `promptViewName(existing)` | `Promise<string \| null>` | A view is being saved. `existing` is the names already used. |
177
+ | `requestReload()` | `void` | The user asks to re-read the layout. |
178
+ | `setAutoReload(on)` | `void` | The user asks to always reload on change. |
179
+ | `onGotoResult({ok, x, y})` | `void` | A `goToPoint` call finished, reporting whether it landed inside. |
180
+ | `isLightTheme()` | `boolean` | The viewer needs to know the theme. Defaults to the OS preference. |
181
+ | `createWorker()` | `Worker` | The parse Worker is needed. Where the scripts cannot be fetched by URL, override this. |
182
+ | `connect(viewer)` | `void` | At mount, handing you the surface described in the following section. |
183
+
184
+ #### The viewer surface
185
+
186
+ `connect(viewer)` gives you the other direction, for pushing into the viewer
187
+ rather than answering it:
188
+
189
+ | Method | Description |
190
+ |---|---|
191
+ | `load(bytes, {reload})` | Display a layout from bytes. |
192
+ | `showError(message)` | Show a fatal error. |
193
+ | `setLyp(name, text)` | Apply layer properties. |
194
+ | `setMarkers(name, text)` | Apply a marker database. |
195
+ | `showStale(text)` | Offer a reload, for when the file changed underneath. |
196
+ | `goToPoint(x, y)` | Center on a coordinate. |
197
+ | `toggleDebug()` | Show or hide the debug panel. |
198
+ | `setNamedViews(views)` | Replace the saved-view set. |
199
+ | `applyTheme()` | Re-ask `isLightTheme()` after a theme change. |
200
+ | `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. |
201
+
202
+ #### Example: a custom host
203
+
204
+ ```js
205
+ window.gdsLensHost = {
206
+ async pickLyp() {
207
+ const text = await fetch("/pdk/layers.lyp").then((r) => r.text());
208
+ return { name: "layers.lyp", text };
209
+ },
210
+ isLightTheme: () => document.documentElement.dataset.theme === "light",
211
+ connect(viewer) {
212
+ myApp.on("layout", (bytes) => viewer.load(bytes));
213
+ }
214
+ };
215
+ ```
216
+
217
+ ### Subpath exports
218
+
219
+ The parsers are pure JavaScript with no DOM and no WebAssembly, so they import
220
+ cleanly into Node, a worker, or an extension host. The package exposes the
221
+ following entry points:
222
+
223
+ | Import | Exports |
224
+ |---|---|
225
+ | `gds-lens` | `GdsLens`, and registers `<gds-lens>`. The bundled module — everything inlined |
226
+ | `gds-lens/parsers` | `parseMarkerFile`, `parseLyrdb`, `parseDrcAscii`, `sniffMarkerFormat`, `parsePointList`, `flattenMarkerModel` |
227
+ | `gds-lens/layout-bytes` | `decodeLayoutBytes`, `looksGzipped`, `gzipStoredSize` |
228
+ | `gds-lens/coord-parse` | `parseCoordinatePair` |
229
+ | `gds-lens/cell-search` | `rankCellMatches`, `cellPathToTarget` |
230
+ | `gds-lens/load-errors` | `describeLoadFailure`, `isOutOfMemory`, `describeDecodeFailure` |
231
+ | `gds-lens/hosts/browser` | `createBrowserHost` |
232
+ | `gds-lens/web/*` | The prebuilt payload, for hosts that serve files rather than bundle |
233
+ | `gds-lens/inline-wasm/*` | The same payload with the binary embedded, for hosts that cannot fetch |
234
+ | `gds-lens/esm/*` | The bundled module by path, if you would rather not rely on `.` |
235
+
236
+ TypeScript declarations ship for all of these.
237
+
238
+ #### The three builds
239
+
240
+ `import "gds-lens"` gets you `dist/esm/`, and that is the right default. The
241
+ other two are there for cases it cannot cover. The following table compares
242
+ them:
243
+
244
+ | Property | `esm` | `web` | `inline-wasm` |
245
+ |---|---|---|---|
246
+ | How it arrives | `import "gds-lens"` | scripts you serve | scripts you serve |
247
+ | Files to serve | none | 4 (+2 optional) | 3 (+2 optional) |
248
+ | Bundler configuration | none | N/A | N/A |
249
+ | Total transfer, gzipped | 252 KB | 235 KB | 243 KB |
250
+ | Streaming WebAssembly compile | no | yes | no |
251
+ | Binary cached separately from the JS | no | yes | no |
252
+ | Sensitive to the page's encoding | no | no | yes |
253
+ | Needs `blob:` in `script-src` | yes | no | no |
254
+
255
+ **`esm`** is one file with everything inside it: the markup, the styles,
256
+ lil-gui, the default host, the WebAssembly binary, and the parse worker's whole
257
+ script. Nothing is fetched, so nothing has to be served or copied, and no
258
+ bundler needs configuring. It costs the streaming compile and about 17 KB over
259
+ the payload.
260
+
261
+ Both the main thread and the parse worker need Emscripten's module, and a
262
+ worker cannot share the main thread's copy. Rather than inline it twice —
263
+ which would add about 190 KB gzipped for nothing — it is inlined once as text
264
+ and loaded from a `blob:` URL by both. That is the one thing this build asks
265
+ of a page's CSP that the others do not: `blob:` in `script-src`, on top of
266
+ the `worker-src blob:` all three need.
267
+
268
+ **`web`** is the payload to serve if you can. `gds-lens-engine.js` fetches
269
+ `gds-lens-engine.wasm` from beside it, so serve the two together; the binary
270
+ compiles as it streams and is cached on its own.
271
+
272
+ **`inline-wasm`** is `web` with the binary embedded in `gds-lens-engine.js`, for
273
+ hosts that cannot fetch a file next to their own scripts — a VS Code webview
274
+ cannot, from a Worker or from the main thread. Nothing else differs.
275
+
276
+ On that last row: `inline-wasm` embeds the binary as a raw string, so
277
+ `gds-lens-engine.js` has to be *decoded* as UTF-8 or it is corrupted, and the module
278
+ then fails with a `WebAssembly.instantiate()` error about section lengths that
279
+ says nothing about the cause. Either `Content-Type: text/javascript;
280
+ charset=utf-8` or `<meta charset="UTF-8">` on the page satisfies it; only the
281
+ absence of both breaks. That payload warns in the console when the document is
282
+ not UTF-8. `esm` escapes its non-ASCII, so it does not care.
283
+
284
+ #### Compressed layouts
285
+
286
+ Gzip is handled for you: `<gds-lens src="chip.gds.gz">` works, and so does a
287
+ plain `.gds` that is secretly gzipped, because the format is decided by magic
288
+ number rather than by filename. Expansion happens in JavaScript rather than
289
+ inside the WebAssembly heap, which is where a second copy of the
290
+ file is most expensive, and it is capped at 2 GB.
291
+
292
+ The same decoder is exported if you want it separately — to check a file before
293
+ handing it over, say:
294
+
295
+ ```js
296
+ import { decodeLayoutBytes } from "gds-lens/layout-bytes";
297
+
298
+ const result = await decodeLayoutBytes(new Uint8Array(await file.arrayBuffer()));
299
+ if (result.ok) await viewer.load(result.bytes);
300
+ else console.error(result.reason); // "too-large" | "corrupt"
301
+ ```
302
+
303
+ ### Limits
304
+
305
+ Parsing, flattening, and triangulating all happen inside a 32-bit WebAssembly
306
+ module, so everything has to fit in one 4 GB address space. Two cases bound
307
+ what fits:
308
+
309
+ - **Flat geometry is the expensive case**. It costs roughly 1 KB per polygon
310
+ end to end, so a couple of million top-level polygons is the practical
311
+ ceiling.
312
+ - **Hierarchy is nearly free**. A cell placed eight or more times becomes a GPU
313
+ instance batch: 24 bytes per placement rather than a full geometry copy. A
314
+ design that flattens to 115 million polygons loads in about 2 GB.
315
+
316
+ Past that the module aborts, and the viewer turns the error into an explanation
317
+ rather than an engine string.
318
+
319
+ ## Build from source
320
+
321
+ Building from source requires the
322
+ [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html)
323
+ (`emcc`/`emcmake` on `PATH`) and Python 3.10 or later for its driver scripts.
324
+ macOS's system `python3` is 3.9 and fails with a `TypeError` on
325
+ `list[str] | None`; with [uv](https://docs.astral.sh/uv/):
326
+
327
+ ```sh
328
+ uv python install 3.13
329
+ export EMSDK_PYTHON="$(uv python find 3.13)"
330
+ ```
331
+
332
+ Then build the payloads and run the tests:
333
+
334
+ ```sh
335
+ git submodule update --init --recursive
336
+ npm install
337
+ npm run build:wasm # all three -> src/wasm/build/{web,inline,esm}/
338
+ npm run build # -> dist/{web,inline-wasm,esm}/
339
+ npm test
340
+ ```
341
+
342
+ `npm run build:wasm:web`, `:inline`, and `:esm` build one variant each; `npm run
343
+ build` then produces whichever outputs it finds the wasm for, and says which it
344
+ skipped. The three differ only in link flags, but CMake caches those, so each
345
+ gets its own build tree.
346
+
347
+ `npm test` includes browser tests that need Chromium
348
+ (`npx playwright install chromium`); they skip if it is missing. The end-to-end
349
+ load test runs once per built payload.
350
+
351
+ Before publishing, `npm run check:dist` and `npm run check:package` verify that
352
+ the payloads are present and no newer than their sources, and that the tarball
353
+ carries nothing it should not. `prepublishOnly` runs both.
354
+
355
+ ## License
356
+
357
+ MIT, see [`LICENCE.md`](LICENCE.md).
358
+
359
+ The payload carries third-party code in two places. Statically linked into the
360
+ WebAssembly: [gdstk](https://github.com/heitzmann/gdstk) (BSL-1.0), Clipper
361
+ (BSL-1.0), [Qhull](http://www.qhull.org) (Qhull license),
362
+ [earcut.hpp](https://github.com/mapbox/earcut.hpp) (ISC), and
363
+ [zlib](https://github.com/madler/zlib) (zlib license) — and `gds-lens-engine.js` is
364
+ itself [Emscripten](https://github.com/emscripten-core/emscripten)'s output
365
+ (MIT/NCSA). Bundled into the JavaScript beside it:
366
+ [lil-gui](https://github.com/georgealways/lil-gui) (MIT).
367
+
368
+ Every notice is reproduced in
369
+ [`THIRD-PARTY-LICENSES.md`](THIRD-PARTY-LICENSES.md). Qhull's license in
370
+ particular requires its notice to accompany any distribution that includes it,
371
+ and its original source can be obtained from
372
+ [the Qhull website](http://www.qhull.org).