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 +167 -0
- package/LICENCE.md +21 -0
- package/README.md +372 -0
- package/THIRD-PARTY-LICENSES.md +261 -0
- package/dist/esm/gds-lens.js +4731 -0
- package/dist/inline-wasm/gds-lens-engine.js +0 -0
- package/dist/inline-wasm/gds-lens-host.js +82 -0
- package/dist/inline-wasm/gds-lens-worker.js +118 -0
- package/dist/inline-wasm/gds-lens.html +29 -0
- package/dist/inline-wasm/gds-lens.js +4705 -0
- package/dist/web/gds-lens-engine.js +2 -0
- package/dist/web/gds-lens-engine.wasm +0 -0
- package/dist/web/gds-lens-host.js +82 -0
- package/dist/web/gds-lens-worker.js +118 -0
- package/dist/web/gds-lens.html +29 -0
- package/dist/web/gds-lens.js +4705 -0
- package/package.json +113 -0
- package/src/cell-search.js +88 -0
- package/src/coord-parse.js +50 -0
- package/src/engine-source.esm.js +49 -0
- package/src/engine-source.js +20 -0
- package/src/gds-lens.js +175 -0
- package/src/hosts/browser.js +156 -0
- package/src/layout-bytes.js +143 -0
- package/src/load-errors.js +64 -0
- package/src/marker-parsers.js +672 -0
- package/src/mount-target.js +23 -0
- package/src/viewer-shell.html +105 -0
- package/src/viewer.css +469 -0
- package/src/viewer.html +29 -0
- package/src/viewer.js +2688 -0
- package/src/wasm-worker.js +157 -0
- package/types/cell-search.d.ts +33 -0
- package/types/coord-parse.d.ts +11 -0
- package/types/gds-lens.d.ts +135 -0
- package/types/hosts-browser.d.ts +13 -0
- package/types/layout-bytes.d.ts +49 -0
- package/types/load-errors.d.ts +27 -0
- package/types/parsers.d.ts +84 -0
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
|
+

|
|
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).
|