Skip to content
EthanLowenthalPublic

About

GDSII & OASIS layout parsing and WebGL2 rendering for the browser — the engine behind the GDS Lens VS Code extension

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

GDS Lens

npm Downloads License: MIT

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.

The viewer: a cell hierarchy tree, a rendered photonic layout, and the layer and display controls

Quick start

npm install gds-lens
import "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 microns

If you would rather serve a payload than bundle one — for a smaller download and a streaming WebAssembly compile — see Embedding GDS Lens.

What it does

  • 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 .lyp file supplies colors, fill styles, and layer names.
  • Browses DRC and LVS markers. The viewer reads .lyrdb report 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 Esc returns 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.

Installation

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.


Reference

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 <gds-lens> element

The element is display: block with no intrinsic height, so give it one. It takes one attribute and exposes a handful of methods.

Attributes

Attribute Description
src URL of a layout to fetch and display. Setting it later reloads.

Properties and methods

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.

Events

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.

Keyboard shortcuts

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.

Several viewers on one page

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.

Removal parks the viewer

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.

Limits

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.

Build from source

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 test

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

License

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.

About

GDSII & OASIS layout parsing and WebGL2 rendering for the browser — the engine behind the GDS Lens VS Code extension

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages