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.
@@ -0,0 +1,157 @@
1
+ import { describeLoadFailure } from "./load-errors.js";
2
+
3
+ // Runs the parse/flatten/triangulate half of loading a layout file (GDSII or
4
+ // OASIS) off the main thread. Either way createGdstkModule is already in
5
+ // scope by the time this file's own code runs, but how it got there depends
6
+ // on the host (see createParseWorker in viewer.js):
7
+ //
8
+ // On an ordinary page the worker is a small blob that importScripts() both
9
+ // gds-lens-engine.js and this file by absolute URL, and the wasm binary is a
10
+ // separate file the module fetches for itself.
11
+ //
12
+ // Some hosts can do neither. A VS Code webview is the worked example: its
13
+ // resource protocol serves `<script src>` tags in the main document fine, but
14
+ // a Worker (even a blob one) can't reach it at all -- importScripts against
15
+ // that URL fails with a NetworkError before it even gets to CSP, and fetch()
16
+ // fails the same way even from the main thread, so the binary can't be
17
+ // fetched either. Such a host uses the inline-wasm build (-sSINGLE_FILE=1,
18
+ // binary embedded in the .js), concatenates its full text with this file's,
19
+ // and returns a Worker built from the result through the ViewerHost's
20
+ // createWorker() hook -- no network involved. See createWorker in
21
+ // hosts/browser.js for the ordinary case this replaces.
22
+ //
23
+ // createGdstkModule() instantiates the *same* wasm module used on the main
24
+ // thread. Its main() calls init_gl(), which fails harmlessly here (no
25
+ // "#glCanvas" -- no DOM at all, same as running under plain Node for
26
+ // headless testing) and returns before touching any DOM/GL state, so this
27
+ // stays a pure computation module in this context: renderer.cpp's
28
+ // parseGdsToLayers() does the parse/flatten/triangulate work and posts
29
+ // 'gdsProgress' messages directly (see report_progress() in renderer.cpp).
30
+ //
31
+ // createGdstkModule() returns a Promise -- a rejection there (or any other
32
+ // async failure below) would otherwise vanish as an unhandled rejection
33
+ // inside this Worker instead of reaching viewer.js's worker.onerror (that
34
+ // only fires for *synchronous* throws), leaving the main thread waiting
35
+ // forever with no error and no progress. Every path below explicitly
36
+ // posts a 'gdsResult' failure instead of letting anything fail silently.
37
+ // Relay console.log/error to the main thread as 'gdsLog' messages -- this
38
+ // worker has no DOM of its own, so viewer.js's on-screen #debugPanel is the
39
+ // only way these are visible without a DevTools window correctly attached to
40
+ // this specific webview (which has proven fiddly to get right). Args are
41
+ // stringified defensively since not everything passed to console.log here
42
+ // (e.g. Error objects, the Module object) is guaranteed structured-cloneable.
43
+ function safeStringify(arg) {
44
+ if (typeof arg === "string") return arg;
45
+ if (arg instanceof Error) return arg.stack || arg.message;
46
+ try {
47
+ return JSON.stringify(arg);
48
+ } catch {
49
+ return String(arg);
50
+ }
51
+ }
52
+ const originalLog = console.log.bind(console);
53
+ const originalError = console.error.bind(console);
54
+ console.log = (...args) => {
55
+ originalLog(...args);
56
+ try {
57
+ postMessage({type: "gdsLog", level: "log", text: args.map(safeStringify).join(" ")});
58
+ } catch {
59
+ // ignore -- best-effort relay only
60
+ }
61
+ };
62
+ console.error = (...args) => {
63
+ originalError(...args);
64
+ try {
65
+ postMessage({type: "gdsLog", level: "error", text: args.map(safeStringify).join(" ")});
66
+ } catch {
67
+ // ignore -- best-effort relay only
68
+ }
69
+ };
70
+
71
+ console.log("[GDS worker] script started executing, typeof createGdstkModule:", typeof createGdstkModule);
72
+
73
+ self.onerror = (msg, url, line, col, err) => {
74
+ console.error("[GDS worker] self.onerror:", msg, "at", url + ":" + line + ":" + col, err && err.stack);
75
+ };
76
+ self.addEventListener("unhandledrejection", (event) => {
77
+ console.error("[GDS worker] unhandled promise rejection inside worker:", event.reason);
78
+ });
79
+
80
+ // Where to fetch gds-lens-engine.wasm from, for the build that keeps it separate.
81
+ // Emscripten would resolve it against this worker's own script URL, which is
82
+ // a blob: with no directory to speak of, so viewer.js passes the real one in.
83
+ // Absent (the inline-wasm build, or a host that assembles the worker itself)
84
+ // there is no binary to locate and the default is left alone.
85
+ function moduleArgs() {
86
+ const base = self.gdsLensScriptBase;
87
+ if (!base) return {};
88
+ return { locateFile: (file) => new URL(file, base).href };
89
+ }
90
+
91
+ console.log("[GDS worker] registering onmessage handler");
92
+ self.onmessage = (event) => {
93
+ const message = event.data;
94
+ console.log("[GDS worker] received message, type:", message.type);
95
+ if (message.type !== "parse") return;
96
+
97
+ console.log("[GDS worker] fileData byteLength:", message.fileData && message.fileData.byteLength);
98
+ console.log("[GDS worker] calling createGdstkModule()...");
99
+ createGdstkModule(moduleArgs()).then((Module) => {
100
+ console.log("[GDS worker] createGdstkModule() resolved, Module keys:", Object.keys(Module).filter(k => typeof Module[k] === "function"));
101
+ // Extension-less name on purpose: GDSII vs OASIS is decided by the
102
+ // file's own header inside the wasm (gds_common::detect_format), so
103
+ // nothing here has to know or plumb through which one this is.
104
+ console.log("[GDS worker] writing /input.layout to MEMFS...");
105
+ Module.FS.writeFile("/input.layout", new Uint8Array(message.fileData));
106
+ console.log("[GDS worker] calling Module.parseGdsToLayers('/input.layout')...");
107
+ const result = Module.parseGdsToLayers("/input.layout");
108
+ console.log("[GDS worker] parseGdsToLayers returned, ok:", result.ok, "format:", result.format, "error:", result.error);
109
+ Module.FS.unlink("/input.layout");
110
+
111
+ if (!result.ok) {
112
+ postMessage({type: "gdsResult", ok: false, error: result.error});
113
+ return;
114
+ }
115
+
116
+ console.log("[GDS worker] layers:", result.layers.length, "instance groups:", result.instanceGroups.length, "labels:", result.totalLabels, "cells:", result.hierarchy.cellCount, "-- posting gdsResult back to main thread");
117
+ const transferList = [];
118
+ for (const layer of result.layers) {
119
+ transferList.push(layer.outlineVertices.buffer, layer.outlineRanges.buffer,
120
+ layer.fillVertices.buffer);
121
+ // Label text (see attach_labels in renderer.cpp). Only the
122
+ // top-level layer entries carry it -- an instanced cell's labels
123
+ // are expanded into world space during the flatten, so the
124
+ // per-group entries below have no text of their own.
125
+ transferList.push(layer.textChars.buffer, layer.textLengths.buffer,
126
+ layer.textOrigins.buffer, layer.textAnchors.buffer);
127
+ }
128
+ for (const group of result.instanceGroups) {
129
+ transferList.push(group.instances.buffer);
130
+ for (const layer of group.layers) {
131
+ transferList.push(layer.outlineVertices.buffer, layer.outlineRanges.buffer,
132
+ layer.fillVertices.buffer);
133
+ }
134
+ }
135
+ // hierarchy (see build_hierarchy in renderer.cpp) is plain objects and
136
+ // numbers -- one entry per cell in the file -- except for each row's
137
+ // per-placement transforms, which are Float64Arrays and so worth
138
+ // transferring rather than cloning. They're capped library-wide
139
+ // (kMaxHierarchyPlacements), so this walk is bounded and most rows
140
+ // (single placements) carry none at all.
141
+ for (const cell of result.hierarchy.cells) {
142
+ for (const ref of cell.refs) {
143
+ if (ref.placements) transferList.push(ref.placements.buffer);
144
+ }
145
+ }
146
+ postMessage(
147
+ {type: "gdsResult", ok: true, layers: result.layers, instanceGroups: result.instanceGroups,
148
+ hierarchy: result.hierarchy, bbox: result.bbox},
149
+ transferList
150
+ );
151
+ console.log("[GDS worker] postMessage(gdsResult) call returned");
152
+ }).catch((err) => {
153
+ console.error("[GDS worker] createGdstkModule() chain rejected:", err, err && err.stack);
154
+ postMessage({type: "gdsResult", ok: false, error: describeLoadFailure(err, "Layout worker failed")});
155
+ });
156
+ };
157
+ console.log("[GDS worker] onmessage handler registered, script finished top-level execution");
@@ -0,0 +1,33 @@
1
+ // `gds-lens/cell-search` -- ranking and pathfinding over a design's cell tree.
2
+ //
3
+ // Both work on the hierarchy model the wasm parse hands back.
4
+
5
+ /** One node of the cell tree. */
6
+ export interface CellNode {
7
+ name?: string;
8
+ /** Child placements, by index into the same array. */
9
+ references?: Array<{ cell: number }>;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * Indices of the cells whose name contains `query`, best first: exact match,
15
+ * then prefix matches, then substring matches. Case-insensitive. Empty when
16
+ * the query is blank or nothing matches.
17
+ */
18
+ export function rankCellMatches(cells: CellNode[], query: string): number[];
19
+
20
+ /**
21
+ * One path of cell indices from a top cell down to `target`, root first and
22
+ * target last -- or `null` if no top cell reaches it.
23
+ *
24
+ * Depth-first, so it is *a* path rather than the shortest: a cell placed in
25
+ * twenty places has twenty paths and none is more correct. Terminates on a
26
+ * reference cycle, and stops descending at `maxDepth`.
27
+ */
28
+ export function cellPathToTarget(
29
+ cells: CellNode[],
30
+ roots: number[],
31
+ target: number,
32
+ maxDepth: number,
33
+ ): number[] | null;
@@ -0,0 +1,11 @@
1
+ // `gds-lens/coord-parse` -- reading a coordinate a person typed.
2
+
3
+ /**
4
+ * A coordinate pair in microns, or `null` if the text is not one.
5
+ *
6
+ * Tolerates the shapes coordinates actually arrive in: bare pairs, wrapped in
7
+ * brackets, comma- or space-separated, and per-number units (`nm`, `um`, `mm`)
8
+ * which are converted. Anything left over after the two numbers rejects the
9
+ * whole string rather than being half-read.
10
+ */
11
+ export function parseCoordinatePair(text: string): { x: number; y: number } | null;
@@ -0,0 +1,135 @@
1
+ // Public types for the `gds-lens` entry point: the custom element, the host
2
+ // interface an embedder implements, and the surface the viewer hands back.
3
+ //
4
+ // Hand-written rather than emitted, because these describe a contract that is
5
+ // deliberately looser than the implementation: every ViewerHost method is
6
+ // optional, and the viewer hides the control for anything a host leaves out.
7
+ // That optionality is the interesting part, and it is the part a generated
8
+ // declaration would get wrong.
9
+
10
+ /** A file a host picked on the viewer's behalf. */
11
+ export interface PickedFile {
12
+ name: string;
13
+ text: string;
14
+ }
15
+
16
+ /** A saved camera position, persisted by the host between sessions. */
17
+ export interface NamedView {
18
+ name: string;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /** The result of a `goToPoint`, reported back to the host. */
23
+ export interface GotoResult {
24
+ ok: boolean;
25
+ x: number;
26
+ y: number;
27
+ }
28
+
29
+ /**
30
+ * What the viewer can be told to do, handed to the host in `connect`.
31
+ *
32
+ * This is the push direction: the host calls these to drive the viewer,
33
+ * rather than answering questions the viewer asks.
34
+ */
35
+ export interface ViewerSurface {
36
+ /**
37
+ * The element the viewer is mounted in. Bind anything of your own to this
38
+ * rather than to `window`, so it stays inside the component -- a listener
39
+ * on `window` reaches the whole embedding page.
40
+ */
41
+ element: HTMLElement;
42
+ load(bytes: Uint8Array | ArrayBuffer, options?: { reload?: boolean }): void;
43
+ showError(message: string): void;
44
+ setLyp(name: string, text: string): void;
45
+ setMarkers(name: string, text: string): void;
46
+ /** Offer a reload, for when the file changed underneath. */
47
+ showStale(text: string): void;
48
+ goToPoint(x: number, y: number): void;
49
+ toggleDebug(): void;
50
+ setNamedViews(views: NamedView[]): void;
51
+ /** Re-ask `isLightTheme()` after a theme change. */
52
+ applyTheme(): void;
53
+ }
54
+
55
+ /**
56
+ * Everything the viewer needs from whatever is embedding it. Install as
57
+ * `window.gdsLensHost` before the element script runs.
58
+ *
59
+ * Every method is optional: a missing one is not an error, it means the
60
+ * embedder does not offer that service, and the viewer removes the control
61
+ * for it. A read-only embed can implement almost none of this.
62
+ */
63
+ export interface ViewerHost {
64
+ /** `null` means the user cancelled. */
65
+ pickLyp?(): Promise<PickedFile | null> | PickedFile | null;
66
+ unloadLyp?(): void;
67
+ pickMarkers?(): Promise<PickedFile | null> | PickedFile | null;
68
+ unloadMarkers?(): void;
69
+ /** Called once at mount, for saved camera positions. */
70
+ loadViews?(): Promise<NamedView[]> | NamedView[];
71
+ saveViews?(views: NamedView[]): void;
72
+ /** `existing` is the names already in use; `null` means cancelled. */
73
+ promptViewName?(existing: string[]): Promise<string | null> | string | null;
74
+ requestReload?(): void;
75
+ setAutoReload?(on: boolean): void;
76
+ onGotoResult?(result: GotoResult): void;
77
+ /** Defaults to the OS preference when not implemented. */
78
+ isLightTheme?(): boolean;
79
+ /** Override where the payload's scripts cannot be fetched by URL. */
80
+ createWorker?(): Worker;
81
+ /** Called at mount, handing over the push-direction surface. */
82
+ connect?(viewer: ViewerSurface): void;
83
+ }
84
+
85
+ /** Accepted by `load`: a URL to fetch, or bytes you already have. */
86
+ export type LayoutSource = string | Uint8Array | ArrayBuffer;
87
+
88
+ /**
89
+ * The `<gds-lens>` element. Importing `gds-lens` registers it; the engine,
90
+ * the wasm module and the WebGL context are all deferred until an element
91
+ * actually connects.
92
+ *
93
+ * `display: block` with no intrinsic height, so give it one.
94
+ */
95
+ export declare class GdsLens extends HTMLElement {
96
+ /**
97
+ * Resolves once the engine has mounted. Every method below awaits this,
98
+ * so it is rarely needed directly. Rejects if the element is not
99
+ * connected.
100
+ */
101
+ readonly ready: Promise<ViewerSurface>;
102
+
103
+ /** `options.reload` keeps the current camera and layer visibility. */
104
+ load(source: LayoutSource, options?: { reload?: boolean }): Promise<void>;
105
+
106
+ /**
107
+ * Centres on a coordinate in microns and flashes a crosshair. Resolves
108
+ * `true` if the point is inside the layout.
109
+ */
110
+ goToPoint(x: number, y: number): Promise<boolean>;
111
+
112
+ /** Applies a `.lyp` layer-properties file. Pass `""` to clear. */
113
+ setLyp(name: string, text: string): Promise<void>;
114
+
115
+ /** Applies a marker database; the format is sniffed from the content. */
116
+ setMarkers(name: string, text: string): Promise<void>;
117
+
118
+ /** Replaces the view with an error message. */
119
+ showError(message: string): Promise<void>;
120
+ }
121
+
122
+ declare global {
123
+ interface HTMLElementTagNameMap {
124
+ "gds-lens": GdsLens;
125
+ }
126
+ interface Window {
127
+ /** Install before the element script runs to replace the default host. */
128
+ gdsLensHost?: ViewerHost;
129
+ /**
130
+ * Published by the default browser host (not by the element), so a
131
+ * plain page can drive the viewer from a script tag or the console.
132
+ */
133
+ gdsLens?: ViewerSurface;
134
+ }
135
+ }
@@ -0,0 +1,13 @@
1
+ // `gds-lens/hosts/browser` -- the default ViewerHost, for a plain web page.
2
+
3
+ import type { ViewerHost } from "./gds-lens.js";
4
+
5
+ /**
6
+ * A host implemented for an ordinary page: `<input type=file>` for the
7
+ * pickers, `localStorage` for saved views, `prompt()` for a name, and three
8
+ * ways in for a layout -- `?src=`, drag-and-drop on the element, and a direct
9
+ * call through `window.gdsLens`.
10
+ *
11
+ * Installs itself as `window.gdsLensHost` on import if nothing else has.
12
+ */
13
+ export function createBrowserHost(): ViewerHost;
@@ -0,0 +1,49 @@
1
+ // `gds-lens/layout-bytes` -- gzip in front of the parser.
2
+ //
3
+ // The result is a discriminated union rather than a throw: "too large for
4
+ // this machine" and "the file is broken" need different things said about
5
+ // them, and the caller is the one with the wording.
6
+
7
+ export interface DecodedLayout {
8
+ ok: true;
9
+ bytes: Uint8Array;
10
+ gzipped: boolean;
11
+ /** gzip's ISIZE trailer, or `null` when the input was too short. */
12
+ storedSize: number | null;
13
+ }
14
+
15
+ export interface FailedLayout {
16
+ ok: false;
17
+ /** `"too-large"` is this machine's limit; `"corrupt"` is the file's. */
18
+ reason: "too-large" | "corrupt";
19
+ storedSize: number | null;
20
+ /** The cap that was enforced, so a message can quote it. */
21
+ limit: number;
22
+ detail: string;
23
+ }
24
+
25
+ /** gzip's magic number (RFC 1952 ID1/ID2). Content, not extension. */
26
+ export function looksGzipped(bytes: Uint8Array | null | undefined): boolean;
27
+
28
+ /**
29
+ * The uncompressed size gzip records in its trailer, or `null` if the input is
30
+ * too short to hold one.
31
+ *
32
+ * A hint about the file rather than a fact about it: it is stored modulo 2^32,
33
+ * and for a multi-member file it describes only the last member. Never use it
34
+ * to enforce a limit -- `decodeLayoutBytes` counts actual output instead.
35
+ */
36
+ export function gzipStoredSize(bytes: Uint8Array | null | undefined): number | null;
37
+
38
+ /**
39
+ * Uncompressed layout bytes, whatever the input was. Non-gzipped input is
40
+ * passed straight back, untouched and uncopied.
41
+ *
42
+ * `maxBytes` caps what a compressed file is allowed to expand to, enforced
43
+ * against the running output total rather than the trailer's claim, so a
44
+ * decompression bomb stops at the cap. Pass a non-finite value for no cap.
45
+ */
46
+ export function decodeLayoutBytes(
47
+ bytes: Uint8Array,
48
+ maxBytes?: number,
49
+ ): Promise<DecodedLayout | FailedLayout>;
@@ -0,0 +1,27 @@
1
+ // `gds-lens/load-errors` -- turning engine-level failures into text a layout
2
+ // engineer can act on.
3
+
4
+ /**
5
+ * A human-readable explanation of a load failure.
6
+ *
7
+ * Running out of room in the 32-bit wasm heap surfaces as one of a handful of
8
+ * unhelpful strings depending on which allocation happened to fail; they all
9
+ * mean the same thing, and all get the same explanation. `prefix` labels
10
+ * anything else with where it came from, and is ignored for out-of-memory,
11
+ * since that is about the layout rather than the component that noticed.
12
+ */
13
+ export function describeLoadFailure(err: unknown, prefix?: string): string;
14
+
15
+ /** Whether a failure is the wasm heap running out, however it surfaced. */
16
+ export function isOutOfMemory(err: unknown): boolean;
17
+
18
+ /**
19
+ * Wording for a failed gzip expansion -- the `{ ok: false }` result from
20
+ * `decodeLayoutBytes`. Returns `""` for a successful one.
21
+ *
22
+ * The two reasons get different prose: `"too-large"` is about this machine's
23
+ * limit, `"corrupt"` about the file being truncated or half-written.
24
+ */
25
+ export function describeDecodeFailure(
26
+ result: { ok: boolean; reason?: string; storedSize?: number | null; limit?: number; detail?: string },
27
+ ): string;
@@ -0,0 +1,84 @@
1
+ // `gds-lens/parsers` -- DRC/LVS marker databases, normalized.
2
+ //
3
+ // Pure JavaScript: no DOM, no WebAssembly. The XML parser is passed in as a
4
+ // constructor argument rather than imported, so this runs in Node (with
5
+ // @xmldom/xmldom) and in a browser (with the built-in DOMParser) unchanged.
6
+
7
+ /** A `DOMParser` constructor -- the platform's, or @xmldom/xmldom's. */
8
+ export type DOMParserConstructor = new () => {
9
+ parseFromString(text: string, type: string): Document;
10
+ };
11
+
12
+ export interface MarkerBBox {
13
+ minX: number;
14
+ minY: number;
15
+ maxX: number;
16
+ maxY: number;
17
+ }
18
+
19
+ /**
20
+ * One marker. Coordinates are in µm, y-up world space.
21
+ *
22
+ * Items with no geometry (float or text values) keep the raw value in `note`,
23
+ * have `bbox === null`, and draw nothing.
24
+ */
25
+ export interface MarkerItem {
26
+ /** Globally unique; equals the index in category-major order. */
27
+ id: number;
28
+ /** Short label for the list row. */
29
+ label: string;
30
+ /** Non-geometry values, multiplicity, cell reference. */
31
+ note: string;
32
+ /** One packed `(x, y, ...)` array per ring. */
33
+ polygons: Float64Array[];
34
+ /** Packed segments: `(x0, y0, x1, y1, ...)`. */
35
+ edges: Float64Array;
36
+ /** `null` when the item has no geometry. */
37
+ bbox: MarkerBBox | null;
38
+ /** True for a `WE<n>` waiver record. */
39
+ waived: boolean;
40
+ }
41
+
42
+ export interface MarkerCategory {
43
+ /** Full path; `.`-joined for lyrdb nesting. */
44
+ name: string;
45
+ description: string;
46
+ items: MarkerItem[];
47
+ }
48
+
49
+ /** The normalized model both parsers emit. */
50
+ export interface MarkerModel {
51
+ /** `""` when the file does not say. */
52
+ topCell: string;
53
+ warnings: string[];
54
+ categories: MarkerCategory[];
55
+ }
56
+
57
+ /** Geometry repacked per item id, ready to hand to the renderer. */
58
+ export interface FlatMarkerModel {
59
+ categories: MarkerCategory[];
60
+ itemCategory: Int32Array;
61
+ itemBBoxes: Float64Array;
62
+ polyVerts: Float64Array;
63
+ polyVertCounts: Int32Array;
64
+ polyItemIds: Int32Array;
65
+ edgeVerts: Float64Array;
66
+ edgeItemIds: Int32Array;
67
+ }
68
+
69
+ /** Decided by content, not by extension. `null` when unrecognized. */
70
+ export function sniffMarkerFormat(text: string): "lyrdb" | "drc" | null;
71
+
72
+ /** Parses a whitespace-separated coordinate list into a packed array. */
73
+ export function parsePointList(text: string): Float64Array;
74
+
75
+ /** Parses a KLayout `.lyrdb` report database. Throws if it is not one. */
76
+ export function parseLyrdb(text: string, DOMParserCtor: DOMParserConstructor): MarkerModel;
77
+
78
+ /** Parses an ASCII DRC results database. */
79
+ export function parseDrcAscii(text: string): MarkerModel;
80
+
81
+ /** Dispatches on `sniffMarkerFormat`. Throws on an unrecognized format. */
82
+ export function parseMarkerFile(text: string, DOMParserCtor: DOMParserConstructor): MarkerModel;
83
+
84
+ export function flattenMarkerModel(model: MarkerModel): FlatMarkerModel;