@file-viewer/renderer-eda 2.0.11

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/dist/eda.js ADDED
@@ -0,0 +1,736 @@
1
+ import { createEdaLayoutWebglBatch, } from '@file-viewer/eda-layout';
2
+ import { parseEdaFile, } from './edaParser';
3
+ const roleLabels = {
4
+ root: '根',
5
+ library: '库',
6
+ symbol: '元件符号',
7
+ footprint: '封装',
8
+ padstack: 'Padstack',
9
+ drawing: '图纸',
10
+ metadata: '元数据',
11
+ property: '属性',
12
+ geometry: '几何',
13
+ net: '网络',
14
+ unknown: '未知',
15
+ };
16
+ const confidenceLabels = {
17
+ high: '高',
18
+ medium: '中',
19
+ low: '低',
20
+ };
21
+ const edaStyle = `
22
+ .eda-viewer{position:relative;height:100%;min-height:0;display:flex;flex-direction:column;background:#edf1f5;color:#172033;box-sizing:border-box}
23
+ .eda-viewer *{box-sizing:border-box}
24
+ .eda-header{min-height:84px;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 176px 18px 22px;border-bottom:1px solid rgba(23,32,51,.08);background:#fff}
25
+ .eda-header span,.eda-panel-head span{color:#0b7480;font-size:12px;font-weight:900;letter-spacing:0}
26
+ .eda-header h2{margin:4px 0 0;font-size:22px;line-height:1.2}
27
+ .eda-header dl{display:grid;grid-template-columns:repeat(4,minmax(70px,auto));gap:10px;margin:0}
28
+ .eda-header dt,.eda-header dd,.eda-entity-group dl,.eda-entity-group dt,.eda-entity-group dd{margin:0}
29
+ .eda-header dt{color:#718096;font-size:12px}
30
+ .eda-header dd{color:#172033;font-weight:900}
31
+ .eda-body{flex:1;min-height:0;display:grid;grid-template-columns:minmax(300px,32%) minmax(0,1fr)}
32
+ .eda-sidebar{min-height:0;display:flex;flex-direction:column;gap:12px;padding:16px;border-right:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.74)}
33
+ .eda-summary,.eda-warning,.eda-panel,.eda-error{border-radius:14px;background:#fff;box-shadow:inset 0 0 0 1px rgba(23,32,51,.06)}
34
+ .eda-summary,.eda-warning{padding:12px}
35
+ .eda-summary strong{display:block;color:#172033}
36
+ .eda-summary p,.eda-warning p,.eda-empty p,.eda-entity-group p{margin:6px 0 0;color:#64748b;line-height:1.55}
37
+ .eda-mini-grid,.eda-stat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
38
+ .eda-mini-grid div,.eda-stat-grid div{min-width:0;padding:10px;border-radius:12px;background:#fff;box-shadow:inset 0 0 0 1px rgba(23,32,51,.06)}
39
+ .eda-mini-grid span,.eda-stat-grid span{display:block;color:#718096;font-size:12px}
40
+ .eda-mini-grid strong,.eda-stat-grid strong{display:block;margin-top:4px;overflow:hidden;color:#172033;font-size:18px;text-overflow:ellipsis;white-space:nowrap}
41
+ .eda-warning{background:#fff7e8;color:#8a4b00}
42
+ .eda-search{height:42px;padding:0 12px;border-radius:12px;border:1px solid rgba(23,32,51,.1);outline:none;background:#fff;font:inherit}
43
+ .eda-stream-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px}
44
+ .eda-stream{min-height:78px;display:grid;grid-template-columns:74px minmax(0,1fr);gap:8px 10px;align-items:center;padding:10px;border:1px solid rgba(23,32,51,.08);border-radius:13px;background:#fff;color:inherit;font:inherit;text-align:left;cursor:pointer}
45
+ .eda-stream:hover,.eda-stream.active,.eda-tree button:hover,.eda-tree button.active,.eda-entity-group button:hover{border-color:rgba(11,116,128,.3);box-shadow:0 10px 22px rgba(23,32,51,.08)}
46
+ .eda-stream span{grid-row:span 3;min-height:40px;display:inline-flex;align-items:center;justify-content:center;padding:0 8px;border-radius:10px;background:rgba(11,116,128,.12);color:#0b7480;font-size:11px;font-weight:900}
47
+ .eda-stream span[data-role='symbol']{background:rgba(34,134,90,.14);color:#1d7a52}
48
+ .eda-stream span[data-role='footprint'],.eda-stream span[data-role='padstack']{background:rgba(111,87,190,.14);color:#5c47a5}
49
+ .eda-stream strong,.eda-stream em,.eda-tree strong,.eda-tree em,.eda-tree small,.eda-entity-group strong,.eda-entity-group span,.eda-entity-group dd{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
50
+ .eda-stream em,.eda-stream small{color:#718096;font-size:12px;font-style:normal}
51
+ .eda-preview{min-width:0;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:14px;padding:16px}
52
+ .eda-panel{min-height:0;overflow:hidden}
53
+ .eda-panel-head{min-height:54px;padding:12px 14px;border-bottom:1px solid rgba(23,32,51,.08)}
54
+ .eda-panel-head strong{display:block;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
55
+ .eda-panel--compact .eda-panel-head{min-height:auto}
56
+ .eda-stat-grid{padding:14px}
57
+ .eda-stat-grid div{background:#f6f9fb}
58
+ .eda-topology,.eda-bottom{min-height:300px;display:grid;grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);gap:14px}
59
+ .eda-topology>.eda-panel{min-height:360px;max-height:min(58vh,620px);display:flex;flex-direction:column}
60
+ .eda-tree,.eda-entities,.eda-diagnostics,.eda-string-grid{min-height:0;max-height:380px;overflow:auto;overscroll-behavior:contain}
61
+ .eda-tree{flex:1;max-height:none;padding:10px}
62
+ .eda-entities{flex:1;max-height:none;padding:12px}
63
+ .eda-tree button{width:100%;min-height:42px;display:grid;grid-template-columns:minmax(22px,auto) minmax(0,1fr) minmax(72px,auto) minmax(72px,auto);gap:8px;align-items:center;margin-bottom:6px;padding:8px;border:1px solid rgba(23,32,51,.06);border-radius:10px;background:#f8fafc;color:inherit;font:inherit;text-align:left;cursor:pointer}
64
+ .eda-tree span{color:#0b7480;font-weight:900}
65
+ .eda-tree em,.eda-tree small{color:#718096;font-size:12px;font-style:normal}
66
+ .eda-entity-group+.eda-entity-group{margin-top:16px}
67
+ .eda-entity-group h3{margin:0 0 8px;color:#172033;font-size:14px}
68
+ .eda-entity-group button{width:100%;display:block;margin-bottom:8px;padding:12px;border:1px solid rgba(23,32,51,.08);border-radius:12px;background:#f8fafc;color:inherit;font:inherit;text-align:left;cursor:pointer}
69
+ .eda-entity-group button>span{display:block;margin-top:4px;color:#718096;font-size:12px}
70
+ .eda-entity-group dl{display:grid;gap:6px;margin-top:10px}
71
+ .eda-entity-group dl div{min-width:0;display:grid;grid-template-columns:90px minmax(0,1fr);gap:8px;color:#475569;font-size:12px}
72
+ .eda-entity-group dt{color:#718096;font-weight:800}
73
+ .eda-selected-meta,.eda-property-grid,.eda-local-strings{display:flex;flex-wrap:wrap;gap:8px;padding:12px 14px 0}
74
+ .eda-selected-meta span,.eda-property-grid div,.eda-local-strings span{min-width:0;display:inline-flex;align-items:center;gap:6px;border-radius:999px;background:#eef6f7;color:#0b7480;font-size:12px;font-weight:800}
75
+ .eda-selected-meta span,.eda-local-strings span{padding:6px 10px}
76
+ .eda-property-grid div{max-width:100%;padding:6px 10px}
77
+ .eda-property-grid span{color:#64748b;font-weight:700}
78
+ .eda-property-grid strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
79
+ .eda-panel pre{min-height:220px;max-height:440px;margin:12px 0 0;overflow:auto;padding:16px;border-top:1px solid rgba(23,32,51,.08);background:#101725;color:#d9e7ff;font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
80
+ .eda-layout-panel{min-height:360px;display:flex;flex-direction:column}
81
+ .eda-layout-meta{display:flex;flex-wrap:wrap;gap:8px;padding:12px 14px;border-bottom:1px solid rgba(23,32,51,.08)}
82
+ .eda-layout-meta span{border-radius:999px;padding:6px 10px;background:#eef6f7;color:#0b7480;font-size:12px;font-weight:800}
83
+ .eda-layout-canvas{flex:1;min-height:320px;overflow:auto;background:#111827}
84
+ .eda-layout-svg{display:block;min-width:860px;min-height:420px;background:#111827}
85
+ .eda-layout-webgl-wrap{position:relative;display:inline-block;min-width:860px;min-height:420px;background:#111827}
86
+ .eda-layout-webgl{display:block;background:#111827}
87
+ .eda-layout-label-layer{position:absolute;inset:0;pointer-events:none;overflow:hidden}
88
+ .eda-layout-label-layer span{position:absolute;max-width:220px;transform:translate(8px,-18px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d9f99d;text-shadow:0 1px 3px #020617;font:700 12px ui-sans-serif,system-ui,sans-serif}
89
+ .eda-layout-svg polygon{fill-opacity:.28;stroke-width:1.4;vector-effect:non-scaling-stroke}
90
+ .eda-layout-svg polyline{fill:none;stroke-width:2;vector-effect:non-scaling-stroke}
91
+ .eda-layout-svg circle{stroke-width:1.4;vector-effect:non-scaling-stroke}
92
+ .eda-layout-svg text{paint-order:stroke;stroke:#111827;stroke-width:3px;stroke-linejoin:round;font:700 13px ui-sans-serif,system-ui,sans-serif}
93
+ .eda-empty{min-height:180px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;text-align:center}
94
+ .eda-string-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));align-content:start;gap:8px;padding:14px}
95
+ .eda-string-grid span{min-width:0;padding:8px 10px;border-radius:10px;background:#f6f9fb;color:#334155;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
96
+ .eda-diagnostics{padding:14px}
97
+ .eda-diagnostics p{margin:0 0 8px;padding:10px;border-radius:10px;background:#f6f9fb;color:#475569;line-height:1.5}
98
+ .eda-diagnostics p[data-level='warning']{background:#fff7e8;color:#8a4b00}
99
+ .eda-diagnostics span{display:inline-flex;margin-right:8px;color:#0b7480;font-size:11px;font-weight:900;text-transform:uppercase}
100
+ .eda-local-strings{padding-bottom:14px}
101
+ .eda-local-strings strong{width:100%;color:#172033;font-size:13px}
102
+ .eda-state{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:12px;background:rgba(237,241,245,.9);z-index:2}
103
+ .eda-state span{width:32px;height:32px;border-radius:999px;border:3px solid rgba(11,116,128,.16);border-top-color:#0b7480;animation:eda-spin .9s linear infinite}
104
+ .eda-error{position:absolute;right:18px;bottom:18px;width:min(440px,calc(100% - 36px));padding:14px;background:#fff7e8;color:#8a4b00;z-index:3}
105
+ @keyframes eda-spin{to{transform:rotate(360deg)}}
106
+ .file-viewer[data-viewer-theme='dark'] .eda-viewer{background:#172033;color:#e5eef8}
107
+ .file-viewer[data-viewer-theme='dark'] .eda-header,.file-viewer[data-viewer-theme='dark'] .eda-summary,.file-viewer[data-viewer-theme='dark'] .eda-panel,.file-viewer[data-viewer-theme='dark'] .eda-sidebar{background:#fff;color:#172033}
108
+ @media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .eda-viewer{background:#172033;color:#e5eef8}.file-viewer[data-viewer-theme='system'] .eda-header,.file-viewer[data-viewer-theme='system'] .eda-summary,.file-viewer[data-viewer-theme='system'] .eda-panel,.file-viewer[data-viewer-theme='system'] .eda-sidebar{background:#fff;color:#172033}}
109
+ @media (max-width:980px){.eda-header,.eda-body,.eda-topology,.eda-bottom{grid-template-columns:1fr}.eda-header{align-items:flex-start;flex-direction:column;padding-right:22px}.eda-body{display:flex;flex-direction:column}.eda-sidebar{max-height:42vh;border-right:0;border-bottom:1px solid rgba(23,32,51,.08)}}
110
+ @media (max-width:640px){.eda-header dl,.eda-mini-grid,.eda-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.eda-tree button{grid-template-columns:minmax(22px,auto) minmax(0,1fr)}.eda-tree em,.eda-tree small{display:none}}
111
+ `;
112
+ const formatBytes = (value) => {
113
+ if (!Number.isFinite(value) || value < 0) {
114
+ return '-';
115
+ }
116
+ if (value < 1024) {
117
+ return `${value} B`;
118
+ }
119
+ const mb = value / 1024 / 1024;
120
+ if (mb >= 1) {
121
+ return `${mb.toFixed(mb < 10 ? 1 : 0)} MB`;
122
+ }
123
+ return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
124
+ };
125
+ const roleLabel = (role) => roleLabels[role] || role;
126
+ const kindLabel = (kind) => {
127
+ return kind === 'storage' ? '目录' : kind === 'text' ? '文本' : '二进制';
128
+ };
129
+ const normalizePath = (value) => value.replace(/^\/+/, '').toLowerCase();
130
+ const flattenTree = (nodes, depth = 0) => {
131
+ return nodes.flatMap(node => [
132
+ { ...node, depth },
133
+ ...flattenTree(node.children, depth + 1),
134
+ ]);
135
+ };
136
+ const createStyle = () => {
137
+ const style = document.createElement('style');
138
+ style.textContent = edaStyle;
139
+ return style;
140
+ };
141
+ const createElement = (tagName, className, text) => {
142
+ const element = document.createElement(tagName);
143
+ if (className) {
144
+ element.className = className;
145
+ }
146
+ if (text !== undefined) {
147
+ element.textContent = text;
148
+ }
149
+ return element;
150
+ };
151
+ const appendDefinition = (list, label, value) => {
152
+ const item = document.createElement('div');
153
+ item.append(createElement('dt', undefined, label), createElement('dd', undefined, value));
154
+ list.append(item);
155
+ };
156
+ const appendPanelHead = (panel, title, value) => {
157
+ const head = createElement('div', 'eda-panel-head');
158
+ head.append(createElement('span', undefined, title), createElement('strong', undefined, value));
159
+ panel.append(head);
160
+ };
161
+ const SVG_NS = 'http://www.w3.org/2000/svg';
162
+ const layoutPalette = [
163
+ '#5eead4',
164
+ '#93c5fd',
165
+ '#c4b5fd',
166
+ '#f9a8d4',
167
+ '#fde68a',
168
+ '#86efac',
169
+ '#fdba74',
170
+ '#67e8f9',
171
+ ];
172
+ const WEBGL_LAYOUT_ELEMENT_THRESHOLD = 360;
173
+ const WEBGL_LAYOUT_VERTEX_THRESHOLD = 1800;
174
+ const WEBGL_FLOATS_PER_VERTEX = 5;
175
+ const layoutColor = (element) => {
176
+ const layer = Number.isFinite(element.layer) ? Number(element.layer) : 0;
177
+ return layoutPalette[Math.abs(layer) % layoutPalette.length];
178
+ };
179
+ const countLayoutVertices = (layout) => {
180
+ return layout.elements.reduce((total, element) => total + element.xy.length, 0);
181
+ };
182
+ const shouldUseWebglLayoutPreview = (layout) => {
183
+ return layout.format === 'gdsii' && (layout.elements.length >= WEBGL_LAYOUT_ELEMENT_THRESHOLD ||
184
+ countLayoutVertices(layout) >= WEBGL_LAYOUT_VERTEX_THRESHOLD);
185
+ };
186
+ const formatOptionalNumber = (value) => {
187
+ if (!Number.isFinite(value)) {
188
+ return '-';
189
+ }
190
+ return Math.abs(Number(value)) < 0.001 ? Number(value).toExponential(2) : String(value);
191
+ };
192
+ const createSvgElement = (tagName, attributes) => {
193
+ const element = document.createElementNS(SVG_NS, tagName);
194
+ Object.entries(attributes).forEach(([key, value]) => {
195
+ element.setAttribute(key, String(value));
196
+ });
197
+ return element;
198
+ };
199
+ const createWebglShader = (gl, type, source) => {
200
+ const shader = gl.createShader(type);
201
+ if (!shader) {
202
+ return null;
203
+ }
204
+ gl.shaderSource(shader, source);
205
+ gl.compileShader(shader);
206
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
207
+ gl.deleteShader(shader);
208
+ return null;
209
+ }
210
+ return shader;
211
+ };
212
+ const createWebglProgram = (gl) => {
213
+ const vertexShader = createWebglShader(gl, gl.VERTEX_SHADER, `
214
+ attribute vec2 a_position;
215
+ attribute vec3 a_color;
216
+ varying vec3 v_color;
217
+ void main() {
218
+ gl_Position = vec4(a_position, 0.0, 1.0);
219
+ gl_PointSize = 5.0;
220
+ v_color = a_color;
221
+ }
222
+ `);
223
+ const fragmentShader = createWebglShader(gl, gl.FRAGMENT_SHADER, `
224
+ precision mediump float;
225
+ varying vec3 v_color;
226
+ uniform float u_alpha;
227
+ void main() {
228
+ gl_FragColor = vec4(v_color, u_alpha);
229
+ }
230
+ `);
231
+ if (!vertexShader || !fragmentShader) {
232
+ return null;
233
+ }
234
+ const program = gl.createProgram();
235
+ if (!program) {
236
+ return null;
237
+ }
238
+ gl.attachShader(program, vertexShader);
239
+ gl.attachShader(program, fragmentShader);
240
+ gl.linkProgram(program);
241
+ gl.deleteShader(vertexShader);
242
+ gl.deleteShader(fragmentShader);
243
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
244
+ gl.deleteProgram(program);
245
+ return null;
246
+ }
247
+ return program;
248
+ };
249
+ const drawWebglVertices = (gl, program, vertices, mode, alpha) => {
250
+ if (!vertices.length) {
251
+ return;
252
+ }
253
+ const buffer = gl.createBuffer();
254
+ if (!buffer) {
255
+ return;
256
+ }
257
+ const stride = WEBGL_FLOATS_PER_VERTEX * Float32Array.BYTES_PER_ELEMENT;
258
+ const positionLocation = gl.getAttribLocation(program, 'a_position');
259
+ const colorLocation = gl.getAttribLocation(program, 'a_color');
260
+ const alphaLocation = gl.getUniformLocation(program, 'u_alpha');
261
+ if (positionLocation < 0 || colorLocation < 0 || !alphaLocation) {
262
+ gl.deleteBuffer(buffer);
263
+ return;
264
+ }
265
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
266
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
267
+ gl.enableVertexAttribArray(positionLocation);
268
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, stride, 0);
269
+ gl.enableVertexAttribArray(colorLocation);
270
+ gl.vertexAttribPointer(colorLocation, 3, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);
271
+ gl.uniform1f(alphaLocation, alpha);
272
+ gl.drawArrays(mode, 0, vertices.length / WEBGL_FLOATS_PER_VERTEX);
273
+ gl.deleteBuffer(buffer);
274
+ };
275
+ const renderWebglBatch = (gl, batch, width, height) => {
276
+ const program = createWebglProgram(gl);
277
+ if (!program) {
278
+ return false;
279
+ }
280
+ gl.viewport(0, 0, width, height);
281
+ gl.clearColor(0.066, 0.094, 0.153, 1);
282
+ gl.clear(gl.COLOR_BUFFER_BIT);
283
+ gl.useProgram(program);
284
+ gl.enable(gl.BLEND);
285
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
286
+ drawWebglVertices(gl, program, batch.triangleVertices, gl.TRIANGLES, 0.32);
287
+ drawWebglVertices(gl, program, batch.lineVertices, gl.LINES, 0.92);
288
+ drawWebglVertices(gl, program, batch.pointVertices, gl.POINTS, 0.98);
289
+ gl.deleteProgram(program);
290
+ return true;
291
+ };
292
+ const createWebglLayoutPreview = (layout, width, height) => {
293
+ const batch = createEdaLayoutWebglBatch(layout, { palette: layoutPalette });
294
+ const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
295
+ const wrap = createElement('div', 'eda-layout-webgl-wrap');
296
+ wrap.style.width = `${width}px`;
297
+ wrap.style.height = `${height}px`;
298
+ const canvas = document.createElement('canvas');
299
+ canvas.className = 'eda-layout-webgl';
300
+ canvas.width = Math.max(1, Math.round(width * devicePixelRatio));
301
+ canvas.height = Math.max(1, Math.round(height * devicePixelRatio));
302
+ canvas.style.width = `${width}px`;
303
+ canvas.style.height = `${height}px`;
304
+ const gl = canvas.getContext('webgl', {
305
+ alpha: false,
306
+ antialias: true,
307
+ preserveDrawingBuffer: true,
308
+ }) || canvas.getContext('experimental-webgl');
309
+ if (!gl || !renderWebglBatch(gl, batch, canvas.width, canvas.height)) {
310
+ return null;
311
+ }
312
+ if (batch.labels.length) {
313
+ const labelLayer = createElement('div', 'eda-layout-label-layer');
314
+ batch.labels.forEach(label => {
315
+ const item = createElement('span', undefined, label.text);
316
+ item.title = label.text;
317
+ item.style.left = `${((label.clipX + 1) / 2) * width}px`;
318
+ item.style.top = `${((1 - label.clipY) / 2) * height}px`;
319
+ labelLayer.append(item);
320
+ });
321
+ wrap.append(canvas, labelLayer);
322
+ }
323
+ else {
324
+ wrap.append(canvas);
325
+ }
326
+ return {
327
+ element: wrap,
328
+ batch,
329
+ };
330
+ };
331
+ const createLayoutPreview = (layout) => {
332
+ const panel = createElement('section', 'eda-panel eda-layout-panel');
333
+ const layoutLabel = layout.format === 'oasis' ? 'OASIS' : 'GDSII';
334
+ appendPanelHead(panel, '版图预览', `${layoutLabel} · ${layout.structureCount || layout.structures.length} structures · ${layout.elements.length} elements`);
335
+ const meta = createElement('div', 'eda-layout-meta');
336
+ [
337
+ `Library: ${layout.libraryName || '-'}`,
338
+ `User unit: ${formatOptionalNumber(layout.userUnit)}`,
339
+ `DB unit: ${formatOptionalNumber(layout.databaseUnit)}`,
340
+ ].forEach(item => meta.append(createElement('span', undefined, item)));
341
+ panel.append(meta);
342
+ if (!layout.bounds || !layout.elements.length) {
343
+ panel.append(createEmpty('没有可绘制几何', `已读取 ${layoutLabel} 头部和 structure 信息,但未发现 boundary、path、text 或 reference 元素。`));
344
+ return panel;
345
+ }
346
+ const bounds = layout.bounds;
347
+ const rawWidth = Math.max(1, bounds.maxX - bounds.minX);
348
+ const rawHeight = Math.max(1, bounds.maxY - bounds.minY);
349
+ const targetWidth = 1200;
350
+ const targetHeight = Math.max(460, Math.min(1100, Math.round(targetWidth * rawHeight / rawWidth)));
351
+ const padding = 40;
352
+ const scale = Math.min((targetWidth - padding * 2) / rawWidth, (targetHeight - padding * 2) / rawHeight);
353
+ const svgWidth = Math.max(860, Math.round(rawWidth * scale + padding * 2));
354
+ const svgHeight = Math.max(420, Math.round(rawHeight * scale + padding * 2));
355
+ const mapPoint = (point) => ({
356
+ x: (point.x - bounds.minX) * scale + padding,
357
+ y: (bounds.maxY - point.y) * scale + padding,
358
+ });
359
+ const pointList = (points) => points
360
+ .map(point => {
361
+ const mapped = mapPoint(point);
362
+ return `${mapped.x.toFixed(2)},${mapped.y.toFixed(2)}`;
363
+ })
364
+ .join(' ');
365
+ const canvas = createElement('div', 'eda-layout-canvas');
366
+ const webglPreview = shouldUseWebglLayoutPreview(layout)
367
+ ? createWebglLayoutPreview(layout, svgWidth, svgHeight)
368
+ : null;
369
+ if (webglPreview) {
370
+ meta.append(createElement('span', undefined, `Renderer: WebGL · ${webglPreview.batch.elementCount} elements`));
371
+ webglPreview.batch.warnings.forEach(item => {
372
+ const warning = createElement('div', 'eda-warning');
373
+ warning.append(createElement('p', undefined, item));
374
+ panel.append(warning);
375
+ });
376
+ canvas.append(webglPreview.element);
377
+ panel.append(canvas);
378
+ return panel;
379
+ }
380
+ meta.append(createElement('span', undefined, 'Renderer: SVG'));
381
+ const svg = createSvgElement('svg', {
382
+ class: 'eda-layout-svg',
383
+ width: svgWidth,
384
+ height: svgHeight,
385
+ viewBox: `0 0 ${svgWidth} ${svgHeight}`,
386
+ role: 'img',
387
+ 'aria-label': `${layoutLabel} layout preview`,
388
+ });
389
+ svg.append(createSvgElement('rect', {
390
+ x: 0,
391
+ y: 0,
392
+ width: svgWidth,
393
+ height: svgHeight,
394
+ fill: '#111827',
395
+ }));
396
+ layout.elements.forEach(element => {
397
+ const color = layoutColor(element);
398
+ if ((element.kind === 'boundary' || element.kind === 'aref') && element.xy.length >= 3) {
399
+ svg.append(createSvgElement('polygon', {
400
+ points: pointList(element.xy),
401
+ fill: color,
402
+ stroke: color,
403
+ }));
404
+ return;
405
+ }
406
+ if (element.kind === 'path' && element.xy.length >= 2) {
407
+ const strokeWidth = Math.max(1.4, Math.min(10, (element.width || rawWidth / 500) * scale));
408
+ svg.append(createSvgElement('polyline', {
409
+ points: pointList(element.xy),
410
+ stroke: color,
411
+ 'stroke-width': strokeWidth,
412
+ }));
413
+ return;
414
+ }
415
+ const anchor = element.xy[0];
416
+ if (!anchor) {
417
+ return;
418
+ }
419
+ const mapped = mapPoint(anchor);
420
+ svg.append(createSvgElement('circle', {
421
+ cx: mapped.x,
422
+ cy: mapped.y,
423
+ r: 4.5,
424
+ fill: '#111827',
425
+ stroke: color,
426
+ }));
427
+ const label = element.text || element.reference || element.kind.toUpperCase();
428
+ if (label) {
429
+ const text = createSvgElement('text', {
430
+ x: mapped.x + 8,
431
+ y: mapped.y - 8,
432
+ fill: color,
433
+ });
434
+ text.append(document.createTextNode(label));
435
+ svg.append(text);
436
+ }
437
+ });
438
+ canvas.append(svg);
439
+ panel.append(canvas);
440
+ return panel;
441
+ };
442
+ const buildStatsCards = (parsed) => {
443
+ const stats = parsed.stats;
444
+ return [
445
+ { label: '文本流', value: stats.textStreams },
446
+ { label: '二进制流', value: stats.binaryStreams },
447
+ { label: '目录', value: stats.storageEntries },
448
+ { label: '属性', value: stats.propertyCount },
449
+ { label: '符号', value: stats.symbolCount },
450
+ { label: '封装', value: stats.footprintCount },
451
+ { label: 'Padstack', value: stats.padstackCount },
452
+ { label: '可信度', value: confidenceLabels[stats.confidence] },
453
+ ];
454
+ };
455
+ const buildEntityGroups = (entities) => {
456
+ const groups = [
457
+ { role: 'symbol', label: '元件符号', items: [] },
458
+ { role: 'footprint', label: '封装图形', items: [] },
459
+ { role: 'padstack', label: 'Padstack', items: [] },
460
+ { role: 'drawing', label: '图纸信息', items: [] },
461
+ ];
462
+ groups.forEach(group => {
463
+ group.items = entities.filter(entity => entity.role === group.role);
464
+ });
465
+ return groups.filter(group => group.items.length);
466
+ };
467
+ const appendStatGrid = (target, items, className) => {
468
+ const grid = createElement('div', className);
469
+ items.forEach(item => {
470
+ const cell = document.createElement('div');
471
+ cell.append(createElement('span', undefined, item.label), createElement('strong', undefined, String(item.value)));
472
+ grid.append(cell);
473
+ });
474
+ target.append(grid);
475
+ };
476
+ const createEmpty = (title, description) => {
477
+ const empty = createElement('div', 'eda-empty');
478
+ empty.append(createElement('strong', undefined, title), createElement('p', undefined, description));
479
+ return empty;
480
+ };
481
+ export default async function renderEda(buffer, target, type = 'olb', context) {
482
+ const normalizedType = ['dra', 'gds', 'oas', 'oasis'].includes(type) ? type : 'olb';
483
+ const filename = context?.filename || `preview.${normalizedType}`;
484
+ const root = createElement('section', 'eda-viewer');
485
+ const style = createStyle();
486
+ const cleanups = [];
487
+ let selectedStream = null;
488
+ let parsedResult = null;
489
+ target.replaceChildren(style, root);
490
+ const listen = (element, event, listener) => {
491
+ element.addEventListener(event, listener);
492
+ cleanups.push(() => element.removeEventListener(event, listener));
493
+ };
494
+ const showLoading = () => {
495
+ const state = createElement('div', 'eda-state');
496
+ state.append(createElement('span'), createElement('strong', undefined, `正在解析 ${normalizedType.toUpperCase()}...`));
497
+ root.append(state);
498
+ return state;
499
+ };
500
+ const showError = (message) => {
501
+ const error = createElement('div', 'eda-error');
502
+ error.append(createElement('strong', undefined, 'EDA 预览提示'), createElement('p', undefined, message));
503
+ root.append(error);
504
+ };
505
+ const renderParsed = (parsed) => {
506
+ parsedResult = parsed;
507
+ selectedStream = parsed.streams.find(stream => stream.properties.length)
508
+ || parsed.streams.find(stream => stream.kind === 'text')
509
+ || parsed.streams[0]
510
+ || null;
511
+ const statsCards = buildStatsCards(parsed);
512
+ const treeRows = flattenTree(parsed.tree);
513
+ const entityGroups = buildEntityGroups(parsed.entities);
514
+ root.replaceChildren();
515
+ const header = createElement('header', 'eda-header');
516
+ const headerTitle = document.createElement('div');
517
+ headerTitle.append(createElement('span', undefined, parsed.parser === 'cfb' ? 'CFB STRUCTURE VIEWER' : 'BINARY STRUCTURE VIEWER'), createElement('h2', undefined, filename));
518
+ const headerStats = document.createElement('dl');
519
+ appendDefinition(headerStats, '格式', parsed.type.toUpperCase());
520
+ appendDefinition(headerStats, '大小', formatBytes(parsed.byteLength));
521
+ appendDefinition(headerStats, '条目', String(parsed.streamCount));
522
+ appendDefinition(headerStats, '可信度', confidenceLabels[parsed.stats.confidence]);
523
+ header.append(headerTitle, headerStats);
524
+ const body = createElement('div', 'eda-body');
525
+ const sidebar = createElement('aside', 'eda-sidebar');
526
+ const summary = createElement('div', 'eda-summary');
527
+ summary.append(createElement('strong', undefined, parsed.title), createElement('p', undefined, parsed.type === 'gds' || parsed.type === 'oas' || parsed.type === 'oasis'
528
+ ? parsed.layout
529
+ ? `${parsed.layout.format === 'oasis' ? 'OASIS' : 'GDSII'} 属于芯片版图工程文件。预览器已在浏览器端解析可识别几何,小图生成 SVG,大图自动切换 WebGL canvas,同时保留结构、字符串和诊断索引。`
530
+ : 'GDSII / OASIS 属于芯片版图工程文件。预览器优先索引结构、属性、可读字符串和二进制线索,并在纯前端安全退化。'
531
+ : 'OLB / DRA 属于 OrCAD / Allegro 生态的私有设计数据。预览器优先解析 CFB 结构、对象候选、属性和可读文本,并在纯前端安全退化。'));
532
+ sidebar.append(summary);
533
+ appendStatGrid(sidebar, statsCards.slice(0, 4), 'eda-mini-grid');
534
+ if (parsed.warnings.length) {
535
+ const warning = createElement('div', 'eda-warning');
536
+ parsed.warnings.forEach(item => warning.append(createElement('p', undefined, item)));
537
+ sidebar.append(warning);
538
+ }
539
+ const search = createElement('input', 'eda-search');
540
+ search.type = 'search';
541
+ search.placeholder = '筛选路径、角色、属性或文本';
542
+ sidebar.append(search);
543
+ const streamList = createElement('div', 'eda-stream-list');
544
+ const preview = createElement('main', 'eda-preview');
545
+ let streamButtons = [];
546
+ const currentPanel = createElement('section', 'eda-panel');
547
+ const selectedHead = createElement('div', 'eda-panel-head');
548
+ const selectedTitle = createElement('span', undefined, '当前条目');
549
+ const selectedPath = createElement('strong', undefined, '未选择');
550
+ selectedHead.append(selectedTitle, selectedPath);
551
+ const selectedMeta = createElement('div', 'eda-selected-meta');
552
+ const selectedProperties = createElement('div', 'eda-property-grid');
553
+ const selectedPreviewContainer = document.createElement('div');
554
+ currentPanel.append(selectedHead, selectedMeta, selectedProperties, selectedPreviewContainer);
555
+ const localStrings = createElement('div', 'eda-local-strings');
556
+ const syncSelection = () => {
557
+ streamButtons.forEach(({ path, button }) => {
558
+ button.classList.toggle('active', normalizePath(path) === normalizePath(selectedStream?.path || ''));
559
+ });
560
+ selectedPath.textContent = selectedStream?.path || '未选择';
561
+ selectedMeta.replaceChildren();
562
+ selectedProperties.replaceChildren();
563
+ selectedPreviewContainer.replaceChildren();
564
+ localStrings.replaceChildren();
565
+ if (!selectedStream) {
566
+ selectedPreviewContainer.append(createEmpty('目录条目', '该节点用于组织下级流,没有可直接展示的文本或十六进制片段。'));
567
+ return;
568
+ }
569
+ selectedMeta.append(createElement('span', undefined, roleLabel(selectedStream.role)), createElement('span', undefined, kindLabel(selectedStream.kind)), createElement('span', undefined, formatBytes(selectedStream.size)));
570
+ selectedStream.properties.forEach(property => {
571
+ const item = document.createElement('div');
572
+ item.append(createElement('span', undefined, property.key), createElement('strong', undefined, property.value));
573
+ selectedProperties.append(item);
574
+ });
575
+ const previewText = selectedStream.sample || selectedStream.hex || '';
576
+ if (previewText) {
577
+ selectedPreviewContainer.append(createElement('pre', undefined, previewText));
578
+ }
579
+ else {
580
+ selectedPreviewContainer.append(createEmpty('目录条目', '该节点用于组织下级流,没有可直接展示的文本或十六进制片段。'));
581
+ }
582
+ if (selectedStream.strings.length) {
583
+ localStrings.append(createElement('strong', undefined, '当前条目字符串'));
584
+ selectedStream.strings.forEach(item => localStrings.append(createElement('span', undefined, item)));
585
+ }
586
+ };
587
+ const selectStream = (stream) => {
588
+ selectedStream = stream;
589
+ syncSelection();
590
+ };
591
+ const selectTreeRow = (row) => {
592
+ const rowPath = normalizePath(row.path);
593
+ const stream = parsed.streams.find(item => normalizePath(item.path) === rowPath);
594
+ if (stream) {
595
+ selectStream(stream);
596
+ }
597
+ };
598
+ const selectEntity = (entity) => {
599
+ const entityPath = normalizePath(entity.path);
600
+ const stream = parsed.streams.find(item => {
601
+ const streamPath = normalizePath(item.path);
602
+ return streamPath === entityPath || streamPath.startsWith(`${entityPath}/`);
603
+ });
604
+ if (stream) {
605
+ selectStream(stream);
606
+ }
607
+ };
608
+ const matchesFilter = (stream, keyword) => {
609
+ if (!keyword) {
610
+ return true;
611
+ }
612
+ const propertyText = stream.properties.map(property => `${property.key}=${property.value}`).join('\n');
613
+ const text = `${stream.path}\n${stream.name}\n${stream.kind}\n${stream.role}\n${stream.sample || ''}\n${stream.strings.join('\n')}\n${propertyText}`.toLowerCase();
614
+ return text.includes(keyword);
615
+ };
616
+ const renderStreams = () => {
617
+ const keyword = search.value.trim().toLowerCase();
618
+ streamList.replaceChildren();
619
+ streamButtons = [];
620
+ parsed.streams.filter(stream => matchesFilter(stream, keyword)).forEach(stream => {
621
+ const button = createElement('button', 'eda-stream');
622
+ button.type = 'button';
623
+ const role = createElement('span', undefined, roleLabel(stream.role));
624
+ role.dataset.role = stream.role;
625
+ button.append(role, createElement('strong', undefined, stream.name || stream.path), createElement('em', undefined, stream.path), createElement('small', undefined, `${kindLabel(stream.kind)} · ${formatBytes(stream.size)}`));
626
+ listen(button, 'click', () => selectStream(stream));
627
+ streamButtons.push({ path: stream.path, button });
628
+ streamList.append(button);
629
+ });
630
+ syncSelection();
631
+ };
632
+ listen(search, 'input', renderStreams);
633
+ sidebar.append(streamList);
634
+ const overview = createElement('section', 'eda-panel eda-panel--compact');
635
+ appendPanelHead(overview, '解析概览', `${parsed.parser.toUpperCase()} · ${formatBytes(parsed.totalStreamBytes)}`);
636
+ appendStatGrid(overview, statsCards, 'eda-stat-grid');
637
+ const topology = createElement('section', 'eda-topology');
638
+ const treePanel = createElement('div', 'eda-panel');
639
+ appendPanelHead(treePanel, '结构树', `${treeRows.length} 节点`);
640
+ const tree = createElement('div', 'eda-tree');
641
+ treeRows.forEach(row => {
642
+ const button = createElement('button');
643
+ button.type = 'button';
644
+ const twist = createElement('span', undefined, row.children.length ? '▸' : '•');
645
+ twist.style.paddingLeft = `${row.depth * 14}px`;
646
+ button.append(twist, createElement('strong', undefined, row.name), createElement('em', undefined, roleLabel(row.role)), createElement('small', undefined, row.size ? formatBytes(row.size) : kindLabel(row.kind)));
647
+ listen(button, 'click', () => selectTreeRow(row));
648
+ tree.append(button);
649
+ });
650
+ treePanel.append(tree);
651
+ const entityPanel = createElement('div', 'eda-panel');
652
+ appendPanelHead(entityPanel, 'EDA 对象', `${parsed.entities.length} 项`);
653
+ if (entityGroups.length) {
654
+ const entityRoot = createElement('div', 'eda-entities');
655
+ entityGroups.forEach(group => {
656
+ const groupRoot = createElement('div', 'eda-entity-group');
657
+ groupRoot.append(createElement('h3', undefined, group.label));
658
+ group.items.forEach(entity => {
659
+ const button = createElement('button');
660
+ button.type = 'button';
661
+ button.append(createElement('strong', undefined, entity.name), createElement('span', undefined, `${formatBytes(entity.byteLength)} · ${entity.streamCount} 条目`));
662
+ if (entity.description) {
663
+ button.append(createElement('p', undefined, entity.description));
664
+ }
665
+ const detail = document.createElement('dl');
666
+ const addDetail = (label, values) => {
667
+ const normalized = Array.isArray(values) ? values.join(', ') : values;
668
+ if (!normalized) {
669
+ return;
670
+ }
671
+ appendDefinition(detail, label, normalized);
672
+ };
673
+ addDetail('Footprint', entity.footprint);
674
+ addDetail('Pins', entity.pins);
675
+ addDetail('Layers', entity.layers);
676
+ addDetail('Keywords', entity.keywords);
677
+ button.append(detail);
678
+ listen(button, 'click', () => selectEntity(entity));
679
+ groupRoot.append(button);
680
+ });
681
+ entityRoot.append(groupRoot);
682
+ });
683
+ entityPanel.append(entityRoot);
684
+ }
685
+ else {
686
+ entityPanel.append(createEmpty('没有明确对象候选', '仍可从结构树、属性和字符串索引中查看可读内容。'));
687
+ }
688
+ topology.append(treePanel, entityPanel);
689
+ const bottom = createElement('section', 'eda-bottom');
690
+ const stringsPanel = createElement('div', 'eda-panel');
691
+ appendPanelHead(stringsPanel, '可读字符串', `${parsed.strings.length} 项`);
692
+ const stringGrid = createElement('div', 'eda-string-grid');
693
+ parsed.strings.forEach(item => stringGrid.append(createElement('span', undefined, item)));
694
+ stringsPanel.append(stringGrid);
695
+ const diagnosticsPanel = createElement('div', 'eda-panel');
696
+ appendPanelHead(diagnosticsPanel, '诊断', `${parsed.diagnostics.length} 条`);
697
+ const diagnostics = createElement('div', 'eda-diagnostics');
698
+ parsed.diagnostics.forEach(diagnostic => {
699
+ const item = createElement('p');
700
+ item.dataset.level = diagnostic.level;
701
+ item.append(createElement('span', undefined, diagnostic.level), document.createTextNode(diagnostic.message));
702
+ diagnostics.append(item);
703
+ });
704
+ diagnosticsPanel.append(diagnostics, localStrings);
705
+ bottom.append(stringsPanel, diagnosticsPanel);
706
+ if (parsed.layout) {
707
+ preview.append(createLayoutPreview(parsed.layout));
708
+ }
709
+ preview.append(overview, topology, currentPanel, bottom);
710
+ body.append(sidebar, preview);
711
+ root.append(header, body);
712
+ renderStreams();
713
+ };
714
+ const loading = showLoading();
715
+ try {
716
+ const parsed = await parseEdaFile(buffer, normalizedType);
717
+ renderParsed(parsed);
718
+ }
719
+ catch (nextError) {
720
+ console.error(nextError);
721
+ root.replaceChildren();
722
+ showError(nextError instanceof Error ? nextError.message : String(nextError));
723
+ }
724
+ finally {
725
+ loading.remove();
726
+ }
727
+ return {
728
+ $el: root,
729
+ unmount() {
730
+ cleanups.splice(0).forEach(cleanup => cleanup());
731
+ parsedResult = null;
732
+ selectedStream = null;
733
+ target.replaceChildren();
734
+ },
735
+ };
736
+ }