gds-lens 0.1.0 → 1.0.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
@@ -4,10 +4,134 @@ Notable changes to this project. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
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.
7
+ From 1.0.0 on, a breaking change to the element's API waits for a major
8
+ version. Before that, `0.x` releases changed it freely.
9
9
 
10
- ## [Unreleased]
10
+ ## [1.0.0] - 2026-08-25
11
+
12
+ ### Added
13
+
14
+ - Several `<gds-lens>` elements can now be live on one page. Each drives its
15
+ own viewer, with its own WebAssembly instance and WebGL2 context, so a `.lyp`
16
+ or marker database applied to one leaves the others alone. A second element
17
+ used to refuse visibly rather than contend for the renderer's state.
18
+ - `destroy()` on the element, releasing a viewer's WebAssembly instance and GL
19
+ context for good. Rarely needed -- an ordinary unmount parks the viewer
20
+ instead, which is what makes a framework remount free.
21
+ - `docs/react.md`: a wrapper component, the JSX type declaration for React 18
22
+ and 19, server rendering, and what remounting does. The examples are covered
23
+ by `test/react.test.js`, and the type declaration by
24
+ `test/types/jsx-smoke.tsx`.
25
+ - `showLoading(label?)` on the viewer surface, for the wait before `load()`:
26
+ a host that is fetching bytes can say so instead of leaving the viewer
27
+ looking idle. The element calls it itself when `load()` is given a URL.
28
+ - `examples/multi-view.html`: six viewers on one page -- three loading a layout
29
+ from `src`, three waiting for a button -- including applying a `.lyp` and a
30
+ marker database to one viewer alone, and creating and releasing a viewer to
31
+ keep a WebGL context free.
32
+ - `docs/embedding.md`, which is where the `ViewerHost` interface, the three
33
+ builds, the subpath exports and the WebAssembly limits moved to. The README
34
+ keeps the quick start and the element's own API and is a third of its previous
35
+ length.
36
+
37
+ ### Fixed
38
+
39
+ - Saved views were shared by every viewer on the page. Each read the whole set
40
+ at mount and wrote the whole set back on save, so two viewers that both saved
41
+ a view overwrote each other and the last one won. The default host now keeps a
42
+ bucket per viewer, keyed by the element's `id`, else its `src`, else -- for a
43
+ page with a single viewer and neither -- the key it has always used. `loadViews`
44
+ and `saveViews` are handed the viewer asking, which is what a host serving
45
+ several needs to tell them apart. A viewer with no `id` and no `src` on a page
46
+ with others has nothing stable to key on: its views last for the life of the
47
+ page. Views previously saved on a page whose element carries a `src` are not
48
+ carried into the new per-layout bucket.
49
+ - The renderer sized its drawing buffer to the window rather than to the canvas
50
+ element, which was only ever right for a viewer filling the page. In an
51
+ embedded `<gds-lens>` the browser stretched a window-sized buffer over the
52
+ element's box, so the layout was drawn distorted; every coordinate the mouse
53
+ produced -- the readout, the ruler, the right-click "Copy coordinate",
54
+ zoom-at-cursor -- answered for a pixel the pointer was not on; and each viewer
55
+ allocated a window-sized buffer and mask texture however small it was on
56
+ screen. It now sizes from the element, and a `ResizeObserver` on the canvas
57
+ keeps it in step with a box that changes without the window changing.
58
+ - The canvas right-click menu passed viewport coordinates to the renderer and
59
+ clamped itself against the window, so in an embedded viewer it opened in the
60
+ wrong place and reported the coordinate of a different pixel.
61
+ - A viewer that had not been asked for a layout showed "Loading layout..." over
62
+ an empty progress bar, which read as a load that had hung. It now says "No
63
+ layout loaded" until something actually asks for one.
64
+ - `import "gds-lens"` shipped no default host. `sideEffects` in package.json
65
+ did not list `src/hosts/browser.js`, whose whole purpose is the side effect
66
+ of installing `window.gdsLensHost`, so every bundler dropped it -- including
67
+ the one that builds `dist/esm`. The documented entry point therefore came up
68
+ with no `.lyp` or marker pickers, no saved views, no drag-and-drop, no
69
+ `window.gdsLens`, and a console error claiming no layout would ever appear on
70
+ pages whose layout had loaded perfectly well. The served payloads were never
71
+ affected, since they load `gds-lens-host.js` as a separate script.
72
+
73
+ The `esm-bundle` test that was meant to catch this asserted on the string
74
+ `gdsLensHost`, which viewer.js contains anyway; it now looks for the host's
75
+ own implementation.
76
+ - A viewer adopted while its WebAssembly module was still starting failed to
77
+ come up at all, reporting `Cannot set properties of null (setting 'width')`.
78
+ `ready` resolves as soon as the viewer is built, which is well before `main()`
79
+ creates the GL context, so an element could be removed and its viewer adopted
80
+ inside that window -- and `adopt` re-pointed the module's DOM root on the next
81
+ turn of the module promise, which is after `main()` has already read it. It
82
+ now writes through the object Emscripten uses *as* the Module, so the new root
83
+ is in place immediately.
84
+ - An element whose parked viewer had been adopted by another element went on
85
+ driving it, so a stray `load()` or `src` change on a detached element wrote
86
+ into whatever was on screen. Such an element now rejects instead.
87
+ - Removing a `<gds-lens>` before its engine finished loading raised an unhandled
88
+ promise rejection, which reached the embedding app's error reporting.
89
+ - An element removed and re-added before its engine arrived built two viewers,
90
+ orphaning the first one's WebAssembly instance and WebGL context behind the
91
+ second's shadow tree.
92
+ - The viewer no longer assigns `window.onerror`, which clobbered the embedding
93
+ page's handler. Page-level failures are reported through an added listener
94
+ and fanned out to whichever debug panels are open.
95
+
96
+ ### Changed
97
+
98
+ - `src/viewer.js` exports `createViewer(element)` instead of doing its work in
99
+ its module body, which is what allows more than one viewer. `window.gdsLensHost`
100
+ is consequently read when each viewer mounts rather than once at import, so an
101
+ app can install a host any time before its first `<gds-lens>` renders.
102
+ - `src/mount-target.js` is gone. It existed only to hand an element to
103
+ viewer.js's module body, which is now a parameter.
104
+
105
+ ## [0.1.1] - 2026-08-25
106
+
107
+ Nothing shipped in this release behaves differently. It exists because 0.1.0
108
+ was published by hand -- a trusted publisher can only be configured on a
109
+ package that already exists -- so this is the first tarball to go through the
110
+ tag-driven workflow, and the first to carry provenance.
111
+
112
+ ### Changed
113
+
114
+ - A shorter README opening.
115
+
116
+ ### Fixed
117
+
118
+ Release tooling only; none of it is in the package.
119
+
120
+ - `eslint .` no longer reads the Emscripten SDK's own config. The CI action
121
+ unpacks the SDK *inside* the working tree, and the SDK ships an
122
+ `eslint.config.mjs` importing a plugin only Emscripten depends on, so lint
123
+ failed with `ERR_MODULE_NOT_FOUND` for a package this project has never
124
+ heard of, before looking at a single file of its own.
125
+ - `check:package` reads `npm pack --dry-run --json` in both shapes npm emits:
126
+ an array of packed-package objects on npm 11, the same objects keyed by
127
+ package name on npm 12. It had destructured the array form, which fails as
128
+ `TypeError: object is not iterable` -- naming neither npm nor a version.
129
+
130
+ Both of these could only fail in the publish job: it is the only place lint
131
+ runs alongside the SDK, and the only place npm is upgraded. `npm` is now
132
+ pinned to `^12` there rather than tracking `latest`, since the floor for
133
+ trusted publishing is 11.5.1 and a release is the worst place to learn that a
134
+ tool changed its output format.
11
135
 
12
136
  ## [0.1.0] - 2026-08-25
13
137
 
@@ -163,5 +287,7 @@ web page rather than for a webview.
163
287
  worker-loading route and shipped unsubstituted. The `createWorker` host hook
164
288
  replaces it.
165
289
 
166
- [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.0...HEAD
290
+ [Unreleased]: https://github.com/EthanLowenthal/GDS-Lens/compare/v1.0.0...HEAD
291
+ [1.0.0]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.1...v1.0.0
292
+ [0.1.1]: https://github.com/EthanLowenthal/GDS-Lens/compare/v0.1.0...v0.1.1
167
293
  [0.1.0]: https://github.com/EthanLowenthal/GDS-Lens/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # GDS Lens
2
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.
3
+ [![npm](https://img.shields.io/npm/v/gds-lens.svg?style=flat-square&color=0f1720&label=NPM)](https://www.npmjs.com/package/gds-lens)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENCE.md)
6
5
 
7
- Reading and rendering only. Writing layouts is deliberately out of scope.
6
+ GDSII and OASIS chip layouts, parsed and rendered in the browser. A drop-in
7
+ custom element compiled to pure JS+WASM with a WebGL2 renderer.
8
8
 
9
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
10
 
11
- > **Status**: pre-1.0. The API is unstable and will change.
11
+ > **Status**: 1.0. The element's API is stable, and breaking changes wait
12
+ > for a major version. See [the changelog](CHANGELOG.md).
12
13
 
13
14
  ## Quick start
14
15
 
@@ -42,41 +43,9 @@ await viewer.load("chip.gds"); // a URL, or bytes you already have
42
43
  await viewer.goToPoint(120.5, -40); // center on a coordinate, in microns
43
44
  ```
44
45
 
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.
46
+ If you would rather serve a payload than bundle one — for a smaller download and
47
+ a streaming WebAssembly compile — see
48
+ [Embedding GDS Lens](docs/embedding.md#serve-the-files-instead-of-bundling).
80
49
 
81
50
  ## What it does
82
51
 
@@ -103,15 +72,18 @@ CI rather than committed.
103
72
 
104
73
  ## Reference
105
74
 
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.
75
+ The following sections describe the element's own API. The rest lives beside
76
+ it:
77
+
78
+ | Page | What is in it |
79
+ |---|---|
80
+ | [React integration](docs/react.md) | A wrapper component, the JSX type declaration, server rendering, remounts. |
81
+ | [Embed the viewer](docs/embedding.md) | The `ViewerHost` interface, the three builds, the subpath exports, and the limits of the WebAssembly module. |
109
82
 
110
83
  ### The `<gds-lens>` element
111
84
 
112
85
  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.
86
+ takes one attribute and exposes a handful of methods.
115
87
 
116
88
  #### Attributes
117
89
 
@@ -133,188 +105,47 @@ The element exposes the following members:
133
105
  | `setLyp(name, text)` | `Promise<void>` | Applies a `.lyp` layer-properties file. Pass `""` to clear. |
134
106
  | `setMarkers(name, text)` | `Promise<void>` | Applies a marker database. The viewer detects the format from the content. |
135
107
  | `showError(message)` | `Promise<void>` | Replaces the view with an error message. |
108
+ | `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). |
136
109
 
137
- #### Only one live element at a time
110
+ #### Several viewers on one page
138
111
 
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.
112
+ Each `<gds-lens>` drives its own viewer, with its own shadow tree, its own
113
+ WebAssembly instance, and its own WebGL2 context. Put as many on a page as you
114
+ like; they share nothing, so a `.lyp` or a marker database applied to one leaves
115
+ the others alone.
142
116
 
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.
117
+ Each one costs a WebGL2 context, though, and browsers cap live contexts per page
118
+ at roughly 8 to 16 — past that the browser starts dropping the oldest. A page
119
+ showing a dozen layouts at once wants one viewer swapping layouts rather than a
120
+ dozen elements.
148
121
 
149
- ### The `ViewerHost` interface
122
+ [`examples/multi-view.html`](examples/multi-view.html) is a working page of six:
123
+ three loading a layout from `src`, three waiting for a button, one of them
124
+ created and released on demand to keep a context free.
150
125
 
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.
126
+ #### Removal parks the viewer
156
127
 
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.
128
+ An element leaving the DOM *parks* its viewer rather than tearing it down, and
129
+ the next `<gds-lens>` to mount without one of its own adopts it. That is what
130
+ makes a framework remount free: the element is new, the viewer is not, and the
131
+ parsed design, the camera and the GL context all survive.
163
132
 
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:
133
+ An element mounted *alongside* a live one finds nothing parked and builds its
134
+ own, which is why the preceding section is true.
167
135
 
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
- ```
136
+ Nothing frees a parked viewer on its own. A page that creates viewers it will
137
+ never show again, and is hitting the context limit, can call `destroy()`; an
138
+ ordinary unmount should let it park. See
139
+ [Remounts and StrictMode](docs/react.md#remounts-and-strictmode) for what this
140
+ means in a framework.
302
141
 
303
142
  ### Limits
304
143
 
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.
144
+ The parser and renderer run in a 32-bit WebAssembly address space, so a layout
145
+ has to fit file plus geometry inside it. See
146
+ [Limits](docs/embedding.md#limits) for the numbers and
147
+ [Compressed layouts](docs/embedding.md#compressed-layouts) for what that means
148
+ for gzipped files.
318
149
 
319
150
  ## Build from source
320
151