gds-lens 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,132 @@ follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
7
7
  From 1.0.0 on, a breaking change to the element's API waits for a major
8
8
  version. Before that, `0.x` releases changed it freely.
9
9
 
10
+ ## [1.3.0] - 2026-09-14
11
+
12
+ ### Added
13
+
14
+ - **Two layouts in one viewer.** A `<gds-lens>` can hold two layouts at once
15
+ and draw them through one camera into one canvas, for comparing two
16
+ revisions of a design:
17
+
18
+ ```js
19
+ await element.load(oldBytes);
20
+ await element.load(newBytes, { slot: "b", name: "rev-b.gds" });
21
+ await element.setBlend(0.5);
22
+ ```
23
+
24
+ - `load(source, { slot })` picks which layout to replace -- `"a"` (the
25
+ default, and the only one a single-layout viewer ever uses) or `"b"`. A
26
+ second layout arriving does not move the camera off what is already being
27
+ read, and loading either slot leaves the other alone.
28
+ - `unload(slot)` drops one again; `setBlend(t)` / `getBlend()` crossfade
29
+ between them, `0` showing only A, `1` only B, between overlaying them.
30
+ - With two loaded, the panel grows a **Compare** folder: the blend slider,
31
+ an optional per-layout tint, and **Highlight differences**, which marks
32
+ per (layer, datatype) where the two disagree -- one colour where only A
33
+ has geometry, another where only B does. Both rasterize through the same
34
+ camera into the same coverage mask in the same frame, so identical
35
+ geometry cancels exactly. It is a difference of what is drawn at the zoom
36
+ being viewed, not a geometric XOR, and differences under about half a
37
+ pixel are ignored; zoom in to resolve a smaller one.
38
+ - The layer list shows the **union** of both layouts' layers, chipped A or B
39
+ where only one has it, so an added or removed layer is visible as a row
40
+ rather than absent. The hierarchy browser roots both cell trees, and cell
41
+ and label searches run over both with the same chips on the hits.
42
+ - `getLayers()` entries gain `source` (0 for slot A, 1 for B), and
43
+ `gds-load`'s detail gains `slot`.
44
+
45
+ See [docs/embedding.md](docs/embedding.md#two-layouts-in-one-viewer). The
46
+ single-layout case is unchanged in every respect: no attribute, no mode
47
+ flag, and the Compare folder is not built at all until a second layout is
48
+ loaded.
49
+
50
+ - **gdsfactory / kfactory ports.** kfactory records each cell's ports as
51
+ KLayout meta info, which KLayout writes into the layout file itself: a
52
+ `$$$CONTEXT_INFO$$$` cell in GDSII, `KLAYOUT_CONTEXT` properties in OASIS,
53
+ each holding strings like `META('kfactory:ports:0')={'name'=>'o1',...}`. The
54
+ viewer now reads those back (`src/wasm/kfactory_ports.cpp`) -- name, type,
55
+ position, direction, width and layer, through the named cross-section --
56
+ and expands them through every placement, so a component's ports are marked
57
+ wherever it sits in the design: a bar across the port, an arrow the way it
58
+ faces, and its name once few enough are on screen to read. Optical ports
59
+ are orange, electrical green, anything else the highlight blue. A **Ports**
60
+ toggle in Display turns the overlay off; a **Ports** folder lists the top
61
+ cell's ports and centers the view on one when clicked. Each hierarchy cell
62
+ entry carries a `ports` array and the hierarchy a `portCount`, the
63
+ `gds-load` event's detail gains `portCount`, and the element exposes nothing
64
+ new otherwise -- a file without the metadata looks exactly as before. The
65
+ expansion stops at 200,000 port placements and says so in the panel.
66
+
67
+ - **`getCamera()` / `setCamera({zoom, panX, panY})`,
68
+ `getMeasurements()` / `addMeasurement(x0, y0, x1, y1)` /
69
+ `clearMeasurements()`, and `getLayers()` / `setLayerVisible(...)`** on the
70
+ viewer surface and the element, for an app that wants to frame the view,
71
+ place a ruler or drive layer visibility itself. Thin pass-throughs over
72
+ behavior that already existed internally; `getLayers()` is also the only way
73
+ to read a layer's name, group, colors and shape counts.
74
+
75
+ ## [1.2.0] - 2026-09-03
76
+
77
+ ### Added
78
+
79
+ - `gds-load` and `gds-error` events on the element, dispatched however a load
80
+ was started -- the `src` attribute, `load()`, or a host pushing bytes through
81
+ its surface. Until now a load that came from `src` could fail only into the
82
+ viewer's own error panel, with nothing for the embedding page to observe.
83
+ `detail` carries `{ layerCount, cellCount }` and `{ message }` respectively.
84
+ Prefixed rather than `load` / `error`, which every HTML element already has
85
+ with other types attached.
86
+ - `showLoading(label?)` on the element. The surface had it; a page fetching its
87
+ own bytes and calling `load(bytes)` could not reach it without `ready`, so it
88
+ sat on "No layout loaded" for the length of the download.
89
+ - A message when WebGL2 cannot be had: unsupported, disabled, or the page over
90
+ its limit on live contexts. The renderer's `main()` returns quietly when its
91
+ context creation fails -- which is what lets the same module run in the parse
92
+ worker and under Node, where there is no canvas -- and nothing on the other
93
+ side ever asked, so the result was a canvas that never drew and a `load()`
94
+ that reported success. The viewer now asks (`isGlReady`) and says so, and a
95
+ lost context (`webglcontextlost`, after a GPU reset or the browser reclaiming
96
+ it) is reported the same way rather than left as a black canvas.
97
+
98
+ ### Changed
99
+
100
+ - `load()` settles on the outcome. It used to resolve as soon as the file had
101
+ been handed to the parse worker, so it never rejected on a file the parser
102
+ refused -- contrary to what the React docs claimed of it. It now resolves
103
+ once the layout is on screen and rejects with the same message the viewer
104
+ shows. A load superseded by a newer one rejects with an error named
105
+ `AbortError`, the name `fetch()` uses for the same thing.
106
+
107
+ - The demo page's layout is served as gzipped OASIS rather than gzipped GDSII:
108
+ the same design, 4.4 MB down from 8.1 MB. `site/make-demo-assets.py` writes
109
+ OASIS now.
110
+
111
+ ### Fixed
112
+
113
+ - The parse worker wrote a dozen lines to the console on every load, into the
114
+ embedding page's DevTools, whether or not tracing had been asked for. The
115
+ relay to the on-screen debug panel was always meant to be unconditional; the
116
+ copy to the real console was not. It is now gated on the same flag as the
117
+ main thread's breadcrumbs (the `debug` attribute or `?gdsDebug=1`). The test
118
+ meant to catch this filtered on `[GDS]`, and the worker's prefix is
119
+ `[GDS worker]`.
120
+ - `destroy()` did not release the viewer. Six listeners on `window` (the
121
+ keyboard shortcuts and the coordinate menu's dismissal) were never removed,
122
+ and a listener's closure holds the whole viewer, wasm instance included; the
123
+ renderer's own `mouseup` and `resize` callbacks on `window` did the same from
124
+ the other side, and the WebGL context waited on garbage collection. Every
125
+ listener now carries an abort signal that `dispose()` fires, and the renderer
126
+ exports a `destroyRenderer()` that unregisters its callbacks and destroys the
127
+ context outright -- so a page calling `destroy()` because it hit the per-page
128
+ context cap gets its slot back when it asks, not when the collector gets to
129
+ it.
130
+ - Two quick changes to `src` (or two `load(url)` calls) could show the older
131
+ layout: the viewer superseded an in-flight *parse*, but the fetch in front of
132
+ it was nobody's to cancel, so a slow first file landing after a small second
133
+ one won. Each `load()` now aborts the fetch before it, and a load parked on
134
+ gzip expansion notices it has been superseded when it wakes.
135
+
10
136
  ## [1.1.0] - 2026-08-31
11
137
 
12
138
  ### Added
@@ -389,7 +515,8 @@ web page rather than for a webview.
389
515
  worker-loading route and shipped unsubstituted. The `createWorker` host hook
390
516
  replaces it.
391
517
 
392
- [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.1.0...HEAD
518
+ [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.2.0...HEAD
519
+ [1.2.0]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.1.0...v1.2.0
393
520
  [1.1.0]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.0.3...v1.1.0
394
521
  [1.0.3]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.0.2...v1.0.3
395
522
  [1.0.2]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.0.1...v1.0.2
package/README.md CHANGED
@@ -15,9 +15,6 @@ a given release does.
15
15
 
16
16
  ![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)
17
17
 
18
- > **Status**: 1.0. The element's API is stable, and breaking changes wait
19
- > for a major version. See [the changelog](CHANGELOG.md).
20
-
21
18
  ## Quick start
22
19
 
23
20
  ```sh
@@ -68,6 +65,14 @@ a streaming WebAssembly compile — see
68
65
  and ASCII DRC results.
69
66
  - **Navigates hierarchy**. You can search cells and labels, and measure
70
67
  distances.
68
+ - **Shows gdsfactory / kfactory ports**. A layout written by gdsfactory 8+
69
+ carries its ports as KLayout metadata inside the file; the viewer reads them
70
+ back, draws every port as a bar across its width with an arrow the way it
71
+ faces (and its name, close in), and lists the top cell's ports in the panel.
72
+ No Python involved.
73
+ - **Compares two layouts**. One viewer can hold two of them, drawn through one
74
+ camera, with a crossfade between them and a per-layer highlight of where they
75
+ differ. See [Two layouts in one viewer](docs/embedding.md#two-layouts-in-one-viewer).
71
76
 
72
77
  ## Installation
73
78
 
@@ -94,8 +99,6 @@ takes one attribute and exposes a handful of methods.
94
99
 
95
100
  #### Attributes
96
101
 
97
- The element takes one attribute:
98
-
99
102
  | Attribute | Description |
100
103
  |---|---|
101
104
  | `src` | URL of a layout to fetch and display. Setting it later reloads. |
@@ -106,13 +109,62 @@ The element exposes the following members:
106
109
 
107
110
  | Member | Returns | Description |
108
111
  |---|---|---|
109
- | `ready` | `Promise<Viewer>` | Resolves once the engine has mounted. Every method in the following table awaits this, so you rarely need it directly. |
110
- | `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. |
112
+ | `ready` | `Promise<ViewerSurface>` | Resolves once the engine has mounted. Every method in the following table awaits this, so you rarely need it directly. |
113
+ | `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](docs/embedding.md#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. |
114
+ | `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. |
111
115
  | `goToPoint(x, y)` | `Promise<boolean>` | Centers on a coordinate in microns and flashes a crosshair. Resolves `true` if the point is inside the layout. |
112
116
  | `setLyp(name, text)` | `Promise<void>` | Applies a `.lyp` layer-properties file. Pass `""` to clear. |
113
117
  | `setMarkers(name, text)` | `Promise<void>` | Applies a marker database. The viewer detects the format from the content. |
114
118
  | `showError(message)` | `Promise<void>` | Replaces the view with an error message. |
115
119
  | `destroy()` | `Promise<void>` | Releases the viewer's WebAssembly instance and WebGL context for good. Rarely needed — see [Removal parks the viewer](#removal-parks-the-viewer). |
120
+ | `getCamera()` | `Promise<{zoom, panX, panY}>` | The current pan/zoom. |
121
+ | `setCamera(camera)` | `Promise<void>` | Sets the pan/zoom. Clamped to the loaded design's bounds (the union of both, with two layouts loaded). |
122
+ | `getLayers()` | `Promise<LayerInfo[]>` | Every layer's name, group, colors, visibility and shape counts, plus the `source` slot it came from. |
123
+ | `setLayerVisible(layer, datatype, visible)` | `Promise<void>` | Shows or hides one layer. |
124
+ | `getMeasurements()` | `Promise<{x0,y0,x1,y1}[]>` | Every ruler on screen, in micron endpoints. |
125
+ | `addMeasurement(x0, y0, x1, y1)` | `Promise<void>` | Places a finished ruler without disturbing measure mode. |
126
+ | `clearMeasurements()` | `Promise<void>` | Removes every ruler. Rulers don't survive a reload either way. |
127
+ | `unload(slot?)` | `Promise<void>` | Drops the second layout, leaving a single-layout viewer. |
128
+ | `setBlend(t)` | `Promise<void>` | Crossfades two loaded layouts: `0` shows only A, `1` only B, between overlays them. |
129
+ | `getBlend()` | `Promise<number>` | The current crossfade. |
130
+
131
+ Every `load()` cancels the one before it *for the same slot*, so two quick
132
+ changes to `src` show the second layout even when the first is the slower
133
+ download.
134
+
135
+ #### Events
136
+
137
+ The element dispatches events on itself. None bubbles.
138
+
139
+ | Event | `detail` | When |
140
+ |---|---|---|
141
+ | `gds-load` | `{ slot, layerCount, cellCount, portCount }` | A layout finished loading and is on screen, whichever way the load was started — the `src` attribute, `load()`, or a host pushing bytes through its surface. `slot` is which of the two layouts it is. `portCount` is how many ports its kfactory metadata declared, 0 for a plain file. |
142
+ | `gds-error` | `{ message }` | A load failed, or `showError()` was called. `message` is the text the viewer shows. |
143
+
144
+ ```js
145
+ viewer.addEventListener("gds-load", (event) => {
146
+ console.log(`${event.detail.layerCount} layers, ${event.detail.cellCount} cells`);
147
+ });
148
+ viewer.addEventListener("gds-error", (event) => {
149
+ reportProblem(event.detail.message);
150
+ });
151
+ ```
152
+
153
+ The names carry a prefix because `load` and `error` already have meanings —
154
+ and types — on every HTML element.
155
+
156
+ #### Keyboard shortcuts
157
+
158
+ The keys act on the viewer that was last pointed at, so on a page with several
159
+ they go to the one under the mouse.
160
+
161
+ | Key | Action |
162
+ |---|---|
163
+ | `[` / `]` | Previous / next marker in the selected marker's category. |
164
+ | `m` | Toggle measure mode. Click two points to place a ruler. |
165
+ | `Esc` | Abandon a ruler being placed; with none in progress, clear the finished ones and return to pan mode. Also closes the coordinate menu. |
166
+ | `h` | Show or hide the cell hierarchy. |
167
+ | `/` | Focus the cell filter box. |
116
168
 
117
169
  #### Several viewers on one page
118
170