GDSII and OASIS chip layouts, parsed and rendered in the browser. A drop-in custom element compiled to pure JS+WASM with a WebGL2 renderer.
Open a layout in your browser to see it
work: drop a .gds or .oas onto the page, or look at the demo layout it
starts with. Nothing is uploaded, and there is nothing to install. The page is
built from the published package, so it is also the quickest way to check what
a given release does.
npm install gds-lensimport "gds-lens"; // registers <gds-lens><gds-lens src="chip.gds" style="width: 100%; height: 600px"></gds-lens>That's the whole thing. Pan with the mouse, zoom with the wheel; on a touch screen, drag to pan and pinch to zoom. The import pulls in one self-contained module — the parser, the renderer, the WebAssembly binary, and the control panel — so there is nothing to copy and nothing else to serve.
Or drive it from JavaScript:
import "gds-lens";
const viewer = document.createElement("gds-lens");
viewer.style.cssText = "width: 100%; height: 600px";
document.body.append(viewer);
await viewer.load("chip.gds"); // a URL, or bytes you already have
await viewer.goToPoint(120.5, -40); // center on a coordinate, in micronsIf you would rather serve a payload than bundle one — for a smaller download and a streaming WebAssembly compile — see Embedding GDS Lens.
- Parses GDSII and OASIS. Format and gzip are both detected from the leading bytes rather than the filename. The reader is gdstk itself, which is what other implementations validate against.
- Renders with WebGL2. Layer-batched vertex buffers, GPU instancing for repeated cells, and a stroke font all live in C++ compiled alongside the parser.
- Reads layer properties. A
.lypfile supplies colors, fill styles, and layer names. - Browses DRC and LVS markers. The viewer reads
.lyrdbreport databases and ASCII DRC results. - Navigates hierarchy. You can search cells and labels, and measure distances.
- Inspects shapes. In Pan mode, click a shape to select it. A card shows its layer, the cell that holds it and the path to that cell from the top, including which placement of a repeated cell it is in, and its bounding box, size, area, perimeter and vertex count. Click the same spot again for the shape under it. The card can frame the shape, copy its details, or show its cell as the new top. With Display > Text on, a click on a drawn text label selects the label, and the card shows its text and anchor point.
- Chooses the top cell. A file with several top cells draws all of them by
default, each as a root row in the hierarchy. The Show as new top
button (⤒ on a row) draws that cell on its own, at its own origin. It works
on any cell, not only a top cell. Back above the tree or
Escreturns to the full layout. - Shows ports. A layout written by a Python layout library can carry its ports as metadata inside the file; the viewer reads them back and lists the top cell's ports in the panel. Display > Ports draws every port as a bar across its width with an arrow the way it faces (and its name, close in). Ports inside placed cells are drawn once you zoom in. No Python involved.
- Compares two layouts. One viewer can hold two of them, drawn through one camera, with a crossfade between them and a per-layer highlight of where they differ. See Two layouts in one viewer.
The package ships prebuilt: no Emscripten toolchain is required to consume it.
Installing straight from a git URL does not work, because dist/ is built in
CI rather than committed.
The following sections describe the element's own API. The rest lives beside it:
| Page | What is in it |
|---|---|
| React integration | A wrapper component, the JSX type declaration, server rendering, remounts. |
| Embed the viewer | The ViewerHost interface, the three builds, the subpath exports, and the limits of the WebAssembly module. |
The element is display: block with no intrinsic height, so give it one. It
takes one attribute and exposes a handful of methods.
| Attribute | Description |
|---|---|
src |
URL of a layout to fetch and display. Setting it later reloads. |
The element exposes the following members:
| Member | Returns | Description |
|---|---|---|
ready |
Promise<ViewerSurface> |
Resolves once the engine has mounted. Every method in the following table awaits this, so you rarely need it directly. |
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. options.slot is "a" (the default) or "b", which loads a second layout alongside the first to compare them — see Two layouts in one viewer. Resolves once the layout is on screen; rejects on a failed fetch, a file the parser refuses, or — with an error named AbortError — when a later load into the same slot superseded this one. |
showLoading(label?) |
Promise<void> |
Shows the loading overlay, for the wait before a load() of bytes the page is fetching itself. load(url) does this on its own. |
goToPoint(x, y) |
Promise<boolean> |
Centers on a coordinate in microns and flashes a crosshair. Resolves true if the point is inside the layout. |
setLyp(name, text) |
Promise<void> |
Applies a .lyp layer-properties file. Pass "" to clear. |
setMarkers(name, text) |
Promise<void> |
Applies a marker database. The viewer detects the format from the content. |
showError(message) |
Promise<void> |
Replaces the view with an error message. |
destroy() |
Promise<void> |
Releases the viewer's WebAssembly instance and WebGL context for good. Rarely needed — see Removal parks the viewer. |
getCamera() |
Promise<{zoom, panX, panY}> |
The current pan/zoom. |
setCamera(camera) |
Promise<void> |
Sets the pan/zoom. Clamped to the loaded design's bounds (the union of both, with two layouts loaded). |
getLayers() |
Promise<LayerInfo[]> |
Every layer's name, group, colors, visibility and shape counts, plus the source slot it came from. |
setLayerVisible(layer, datatype, visible) |
Promise<void> |
Shows or hides one layer. |
getMeasurements() |
Promise<{x0,y0,x1,y1}[]> |
Every ruler on screen, in micron endpoints. |
addMeasurement(x0, y0, x1, y1) |
Promise<void> |
Places a finished ruler without disturbing measure mode. |
clearMeasurements() |
Promise<void> |
Removes every ruler. Rulers don't survive a reload either way. |
unload(slot?) |
Promise<void> |
Drops the second layout, leaving a single-layout viewer. |
setBlend(t) |
Promise<void> |
Crossfades two loaded layouts: 0 shows only A, 1 only B, between overlays them. |
getBlend() |
Promise<number> |
The current crossfade. |
getTopCells(slot?) |
Promise<{cells, current}> |
The file's own top cells, and the cell drawn as the top (null when all of them are drawn). |
setTopCell(name, slot?) |
Promise<void> |
Draws one cell as the top: only that cell and what it places, at its own origin, framed. Any cell can be named. null draws every top cell again. Rejects if the layout has no cell by that name. |
getSelection() |
Promise<ShapeInfo | null> |
The shape or text label selected by a click on the canvas or by selectAt(), or null. kind is "polygon" or "label". |
selectAt(x, y, index?) |
Promise<ShapeInfo | null> |
Selects the shape under a point in microns, as a click there does, and resolves to it. While text is drawn, a label whose drawn text covers the point is selected too. index picks the one that many after the first, in order of how near each shape's outline or label's text is to the point. Resolves null, and clears the selection, when nothing on a visible layer is there. |
clearSelection() |
Promise<void> |
Clears the selection. |
Every load() cancels the one before it for the same slot, so two quick
changes to src show the second layout even when the first is the slower
download.
The element dispatches events on itself. None bubbles.
| Event | detail |
When |
|---|---|---|
gds-load |
{ slot, layerCount, cellCount, portCount, topCell } |
A layout finished loading and is on screen, whichever way the load was started: the src attribute, load(), setTopCell(), or a host pushing bytes through its surface. slot is which of the two layouts it is. portCount is how many ports its port metadata declared, 0 for a plain file. topCell is the cell drawn as the top, or null when every top cell is drawn. |
gds-error |
{ message } |
A load failed, or showError() was called. message is the text the viewer shows. |
gds-select |
ShapeInfo or null |
The selection changed: a click on the canvas, selectAt(), Esc, or a reload of the layout the shape is in. null means it was cleared. See Selecting a shape for the fields of a selected label. |
viewer.addEventListener("gds-load", (event) => {
console.log(`${event.detail.layerCount} layers, ${event.detail.cellCount} cells`);
});
viewer.addEventListener("gds-error", (event) => {
reportProblem(event.detail.message);
});The names carry a prefix because load and error already have meanings —
and types — on every HTML element.
The keys act on the viewer that was last pointed at, so on a page with several they go to the one under the mouse.
| Key | Action |
|---|---|
[ / ] |
Previous / next marker in the selected marker's category. |
m |
Toggle measure mode. Click two points to place a ruler. |
Esc |
Abandon a ruler being placed; with none in progress, clear the finished ones and return to pan mode. Also closes the coordinate menu, and clears the selected shape and hierarchy row. With none of that to do, go back from a cell shown as the top cell to the full layout. |
h |
Show or hide the cell hierarchy. |
/ |
Focus the cell filter box. |
The Keyboard Shortcuts button at the bottom of the panel's Display folder lists these keys in a dialog. A host can take over the letter and bracket keys and bind its own; the dialog then lists the host's bindings. See Keyboard shortcuts in the embedding guide.
Each <gds-lens> drives its own viewer, with its own shadow tree, its own
WebAssembly instance, and its own WebGL2 context. Put as many on a page as you
like; they share nothing, so a .lyp or a marker database applied to one leaves
the others alone.
Each one costs a WebGL2 context, though, and browsers cap live contexts per page at roughly 8 to 16 — past that the browser starts dropping the oldest. A page showing a dozen layouts at once wants one viewer swapping layouts rather than a dozen elements.
examples/multi-view.html
is a working page of six: three loading a layout from src, three waiting for a
button, one of them created and released on demand to keep a context free.
An element leaving the DOM parks its viewer rather than tearing it down, and
the next <gds-lens> to mount without one of its own adopts it. That is what
makes a framework remount free: the element is new, the viewer is not, and the
parsed design, the camera and the GL context all survive.
An element mounted alongside a live one finds nothing parked and builds its own, which is why the preceding section is true.
Nothing frees a parked viewer on its own. A page that creates viewers it will
never show again, and is hitting the context limit, can call destroy(); an
ordinary unmount should let it park. See
Remounts and StrictMode for what this
means in a framework.
The parser and renderer run in a 32-bit WebAssembly address space, so a layout has to fit — file plus geometry — inside it. See Limits for the numbers and Compressed layouts for what that means for gzipped files.
Building from source requires the
Emscripten SDK
(emcc/emcmake on PATH) and Python 3.10 or later for its driver scripts.
macOS's system python3 is 3.9 and fails with a TypeError on
list[str] | None; with uv:
uv python install 3.13
export EMSDK_PYTHON="$(uv python find 3.13)"Then build the payloads and run the tests:
git submodule update --init --recursive
npm install
npm run build:wasm # all three -> src/wasm/build/{web,inline,esm}/
npm run build # -> dist/{web,inline-wasm,esm}/
npm testnpm run build:wasm:web, :inline, and :esm build one variant each; npm run build then produces whichever outputs it finds the wasm for, and says which it
skipped. The three differ only in link flags, but CMake caches those, so each
gets its own build tree.
npm test includes browser tests that need Chromium
(npx playwright install chromium); they skip if it is missing. The end-to-end
load test runs once per built payload.
Before publishing, npm run check:dist and npm run check:package verify that
the payloads are present and no newer than their sources, and that the tarball
carries nothing it should not. prepublishOnly runs both.
MIT, see LICENCE.md.
The payload carries third-party code in two places. Statically linked into the
WebAssembly: gdstk (BSL-1.0), Clipper
(BSL-1.0), Qhull (Qhull license),
earcut.hpp (ISC), and
zlib (zlib license) — and gds-lens-engine.js is
itself Emscripten's output
(MIT/NCSA). Bundled into the JavaScript beside it:
lil-gui (MIT).
Every notice is reproduced in
THIRD-PARTY-LICENSES.md. Qhull's license in
particular requires its notice to accompany any distribution that includes it,
and its original source can be obtained from
the Qhull website.
