gerbers-renderer 0.1.3 โ 1.1.4
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/README.md +725 -8
- package/dist/core/detect.d.ts +2 -0
- package/dist/core/errors.d.ts +5 -0
- package/dist/core/list.d.ts +2 -0
- package/dist/core/types.d.ts +18 -0
- package/dist/gerbers-renderer.es.js +3642 -2240
- package/dist/gerbers-renderer.es.js.map +1 -1
- package/dist/gerbers-renderer.umd.js +28 -46
- package/dist/gerbers-renderer.umd.js.map +1 -1
- package/dist/index.d.ts +26 -3
- package/dist/io/unpackArchive.d.ts +9 -0
- package/dist/libarchive-Bt1VdZR0.js +272 -0
- package/dist/libarchive-Bt1VdZR0.js.map +1 -0
- package/dist/parse/gerber-parser.d.ts +28 -2
- package/dist/render/renderGerbers.d.ts +3 -0
- package/dist/render/renderGerbersFiles.d.ts +7 -0
- package/dist/render/renderGerbersZip.d.ts +3 -7
- package/dist/render-pipeline/core/renderContract.d.ts +98 -0
- package/dist/render-pipeline/core/renderScheduler.d.ts +9 -0
- package/dist/render-pipeline/core/testSetup.d.ts +0 -0
- package/dist/render-pipeline/core/viewportTransform.d.ts +32 -0
- package/dist/render-pipeline/core/viewportTransform.example.d.ts +65 -0
- package/dist/render-pipeline/core/viewportTransform.test.d.ts +1 -0
- package/dist/render-pipeline/eventSystem.test.d.ts +1 -0
- package/dist/render-pipeline/events.d.ts +9 -0
- package/dist/render-pipeline/exampleOverlays.d.ts +12 -0
- package/dist/render-pipeline/integratedViewer.d.ts +23 -0
- package/dist/render-pipeline/markerPass.d.ts +6 -0
- package/dist/render-pipeline/markerPicker.d.ts +7 -0
- package/dist/render-pipeline/markerRenderer.d.ts +11 -0
- package/dist/render-pipeline/markerStore.d.ts +17 -0
- package/dist/render-pipeline/markerSystem.test.d.ts +1 -0
- package/dist/render-pipeline/overlayPass.d.ts +3 -0
- package/dist/render-pipeline/overlayRegistry.d.ts +13 -0
- package/dist/render-pipeline/overlayRegistry.test.d.ts +1 -0
- package/dist/render-pipeline/renderPasses.d.ts +78 -0
- package/dist/render-pipeline/uniformGridIndex.d.ts +10 -0
- package/dist/render-pipeline/viewer.d.ts +93 -0
- package/dist/render-pipeline/viewer.test.d.ts +1 -0
- package/dist/render-pipeline/viewerEvents.d.ts +23 -0
- package/dist/render-pipeline/visibilityManager.d.ts +20 -0
- package/dist/render-pipeline/visibilityPlumbing.test.d.ts +1 -0
- package/dist/viewer/types.d.ts +0 -9
- package/package.json +6 -3
- package/dist/viewer/BoardViewer.d.ts +0 -5
package/README.md
CHANGED
|
@@ -15,11 +15,18 @@ Designed for:
|
|
|
15
15
|
|
|
16
16
|
- ๐ง Gerber bundle detection (not just โtry and failโ)
|
|
17
17
|
- ๐ฆ Supports `.zip` and `.rar` archives (browser-side)
|
|
18
|
-
- ๐จ
|
|
18
|
+
- ๐จ Canvas-based board viewer with modern render pipeline
|
|
19
19
|
- ๐งฉ Drop-in viewer that mounts into any DOM node
|
|
20
|
+
- ๐ฏ Overlay system for custom visualizations
|
|
21
|
+
- ๐ High-performance marker system with spatial indexing
|
|
22
|
+
- ๐ฏ Built-in selection and interaction systems
|
|
23
|
+
- โก Typed event emitter for user interactions
|
|
24
|
+
- ๐๏ธ Centralized visibility management system
|
|
20
25
|
- ๐งช Typed, deterministic render results
|
|
21
26
|
- ๐งผ No backend, no workers unless needed
|
|
22
27
|
- โก Vite, React, vanilla JS friendly
|
|
28
|
+
- ๐ฏ Precise viewport transforms with camera controls
|
|
29
|
+
- ๐ Coordinate system: Board (mm) โ Screen (px) conversion
|
|
23
30
|
|
|
24
31
|
## Installation
|
|
25
32
|
|
|
@@ -27,12 +34,13 @@ Designed for:
|
|
|
27
34
|
npm install gerbers-renderer
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
## Quick start
|
|
37
|
+
## Quick start
|
|
31
38
|
|
|
39
|
+
### Integrated Viewer (Canvas-based)
|
|
32
40
|
```typescript
|
|
33
|
-
import {
|
|
41
|
+
import { createIntegratedViewer } from "gerbers-renderer";
|
|
34
42
|
|
|
35
|
-
const viewer =
|
|
43
|
+
const viewer = createIntegratedViewer(document.getElementById("pcb")!);
|
|
36
44
|
|
|
37
45
|
const file = input.files[0];
|
|
38
46
|
const buffer = await file.arrayBuffer();
|
|
@@ -46,8 +54,29 @@ viewer.setData({
|
|
|
46
54
|
layers: result.layers,
|
|
47
55
|
});
|
|
48
56
|
viewer.fit();
|
|
57
|
+
|
|
58
|
+
viewer.setSelection({
|
|
59
|
+
type: "region",
|
|
60
|
+
bounds: { min: { x: 0, y: 0 }, max: { x: 20, y: 10 } }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Add markers for DFM analysis
|
|
64
|
+
viewer.addMarker({
|
|
65
|
+
id: "via-issue",
|
|
66
|
+
x_mm: 12.5,
|
|
67
|
+
y_mm: 8.3,
|
|
68
|
+
severity: "error",
|
|
69
|
+
data: { issue: "Via too close to trace" }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
viewer.addMarkers([
|
|
73
|
+
{ id: "test1", x_mm: 5, y_mm: 5, severity: "info" },
|
|
74
|
+
{ id: "test2", x_mm: 15, y_mm: 10, severity: "warning" }
|
|
75
|
+
]);
|
|
49
76
|
```
|
|
50
77
|
|
|
78
|
+
**Documentation**: See [MIGRATION.md](./MIGRATION.md) for detailed usage guide.
|
|
79
|
+
|
|
51
80
|
Always call `result.revoke()` when replacing a render.
|
|
52
81
|
|
|
53
82
|
## Live demo
|
|
@@ -61,6 +90,14 @@ npm run dev
|
|
|
61
90
|
Open:
|
|
62
91
|
๐ http://localhost:5173/demo/
|
|
63
92
|
|
|
93
|
+
**The demo now showcases the new integrated viewer** with:
|
|
94
|
+
- Canvas-based rendering with hardware acceleration
|
|
95
|
+
- Grid overlay with mm/in units
|
|
96
|
+
- Precise viewport transforms and smooth pan/zoom
|
|
97
|
+
- Mouse-centered zoom (zooms where cursor is positioned)
|
|
98
|
+
- Green FR4 board background
|
|
99
|
+
- Clean rendering without placeholder text artifacts
|
|
100
|
+
|
|
64
101
|
## Supported input formats
|
|
65
102
|
|
|
66
103
|
| Format | Supported | Notes |
|
|
@@ -140,7 +177,7 @@ renderGerbersFiles(
|
|
|
140
177
|
Create a drop-in board viewer:
|
|
141
178
|
|
|
142
179
|
```typescript
|
|
143
|
-
const viewer =
|
|
180
|
+
const viewer = createIntegratedViewer(container, {
|
|
144
181
|
onDownload: () => {
|
|
145
182
|
/* optional */
|
|
146
183
|
},
|
|
@@ -157,10 +194,13 @@ viewer.fit();
|
|
|
157
194
|
|
|
158
195
|
The viewer supports:
|
|
159
196
|
|
|
160
|
-
- Pan / zoom
|
|
161
|
-
- Layer toggling
|
|
162
|
-
-
|
|
197
|
+
- Pan / zoom with mouse-centered zoom
|
|
198
|
+
- Layer toggling (top/bottom switching)
|
|
199
|
+
- Grid overlay with mm/in units
|
|
200
|
+
- Custom overlays and markers
|
|
201
|
+
- Selection regions
|
|
163
202
|
- Download hook (original Gerbers)
|
|
203
|
+
- Hardware-accelerated canvas rendering
|
|
164
204
|
|
|
165
205
|
## Return value
|
|
166
206
|
|
|
@@ -238,6 +278,683 @@ This library is intentionally focused on fast, accurate visualization.
|
|
|
238
278
|
- WASM-only core split
|
|
239
279
|
- Headless CI validation mode
|
|
240
280
|
|
|
281
|
+
## Architecture
|
|
282
|
+
|
|
283
|
+
### Viewport Transform System
|
|
284
|
+
|
|
285
|
+
The renderer uses a precise coordinate transformation system:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { ViewportTransform, CameraState } from "gerbers-renderer";
|
|
289
|
+
|
|
290
|
+
const transform = new ViewportTransform(
|
|
291
|
+
{ center_mm: { x: 50, y: 25 }, zoom: 10 },
|
|
292
|
+
{ width_px: 800, height_px: 600 }
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Convert between coordinate systems
|
|
296
|
+
const screenPos = transform.boardToScreen({ x: 10, y: 5 }); // mm โ px
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This enables:
|
|
300
|
+
- Precise coordinate transformations
|
|
301
|
+
- Smooth pan/zoom operations
|
|
302
|
+
- Mathematical foundation for extensions
|
|
303
|
+
|
|
304
|
+
## Overlay System
|
|
305
|
+
|
|
306
|
+
The integrated viewer includes a powerful overlay system for custom visualizations:
|
|
307
|
+
|
|
308
|
+
### Adding Custom Overlays
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { createViolationDotsOverlay, createGridOverlay } from "gerbers-renderer";
|
|
312
|
+
|
|
313
|
+
// Add DFM violation dots (world space)
|
|
314
|
+
viewer.addOverlayLayer({
|
|
315
|
+
id: "dfm:dots",
|
|
316
|
+
zIndex: 50,
|
|
317
|
+
visible: true,
|
|
318
|
+
drawInWorldSpace: true, // Draw in mm coordinates
|
|
319
|
+
draw: (ctx, api) => {
|
|
320
|
+
const violations = [
|
|
321
|
+
{ x_mm: 10, y_mm: 12 },
|
|
322
|
+
{ x_mm: 40, y_mm: 5 }
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
ctx.fillStyle = 'red';
|
|
326
|
+
for (const v of violations) {
|
|
327
|
+
ctx.beginPath();
|
|
328
|
+
ctx.arc(v.x_mm, v.y_mm, 0.25, 0, Math.PI * 2); // 0.25mm radius
|
|
329
|
+
ctx.fill();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Add tooltip overlay (screen space)
|
|
335
|
+
viewer.addOverlayLayer({
|
|
336
|
+
id: "ui:tooltip",
|
|
337
|
+
zIndex: 200,
|
|
338
|
+
visible: true,
|
|
339
|
+
drawInWorldSpace: false, // Draw in screen pixels
|
|
340
|
+
draw: (ctx, api) => {
|
|
341
|
+
const hover = getCurrentHover(); // Get hover state
|
|
342
|
+
if (!hover) return;
|
|
343
|
+
|
|
344
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
|
345
|
+
ctx.fillRect(hover.x_px + 12, hover.y_px - 20, 100, 20);
|
|
346
|
+
ctx.fillStyle = 'white';
|
|
347
|
+
ctx.fillText(hover.text, hover.x_px + 15, hover.y_px - 5);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Overlay API
|
|
353
|
+
|
|
354
|
+
Overlays receive a stable API object:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
type OverlayApi = {
|
|
358
|
+
// Coordinate conversion
|
|
359
|
+
boardToScreen: (p: { x_mm: number; y_mm: number }) => { x_px: number; y_px: number };
|
|
360
|
+
screenToBoard: (p: { x_px: number; y_px: number }) => { x_mm: number; y_mm: number };
|
|
361
|
+
|
|
362
|
+
// View state
|
|
363
|
+
getViewState: () => { center_mm: { x: number; y: number }; zoom: number; rotation_rad: number };
|
|
364
|
+
getViewport: () => { width_px: number; height_px: number };
|
|
365
|
+
getBoardBounds: () => { minX_mm: number; minY_mm: number; maxX_mm: number; maxY_mm: number };
|
|
366
|
+
|
|
367
|
+
// Render control
|
|
368
|
+
requestRender: (reason: string) => void;
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Built-in Overlay Examples
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Grid overlay
|
|
376
|
+
viewer.addOverlayLayer(createGridOverlay(1)); // 1mm spacing
|
|
377
|
+
|
|
378
|
+
// Violation dots
|
|
379
|
+
viewer.addOverlayLayer(createViolationDotsOverlay());
|
|
380
|
+
|
|
381
|
+
// Animated marker
|
|
382
|
+
viewer.addOverlayLayer(createPulsingMarkerOverlay({ x_mm: 25, y_mm: 30 }));
|
|
383
|
+
|
|
384
|
+
// Tooltip (provide hover state)
|
|
385
|
+
viewer.addOverlayLayer(createTooltipOverlay(() => getCurrentHover()));
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Overlay Management
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Control visibility
|
|
392
|
+
viewer.setOverlayVisibility("dfm:dots", false);
|
|
393
|
+
|
|
394
|
+
// Remove overlay
|
|
395
|
+
viewer.removeOverlay("ui:tooltip");
|
|
396
|
+
|
|
397
|
+
// Access registry directly
|
|
398
|
+
const registry = viewer.getOverlayRegistry();
|
|
399
|
+
registry.setZIndex("dfm:dots", 100); // Change render order
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Key Features:**
|
|
403
|
+
- **Stable API**: Same object reference, current state access
|
|
404
|
+
- **Explicit coordinate spaces**: World (mm) vs Screen (px) drawing
|
|
405
|
+
- **Efficient rendering**: Sorted by zIndex, filtered by visibility
|
|
406
|
+
- **Animation support**: Use `api.requestRender()` for smooth animations
|
|
407
|
+
- **Lifecycle hooks**: `onAdd()` and `onRemove()` for setup/cleanup
|
|
408
|
+
|
|
409
|
+
## Marker System
|
|
410
|
+
|
|
411
|
+
The integrated viewer includes a high-performance marker system for interactive annotations, test points, and DFM indicators:
|
|
412
|
+
|
|
413
|
+
### Adding Markers
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Add individual markers
|
|
417
|
+
viewer.addMarker({
|
|
418
|
+
id: "test-point-1",
|
|
419
|
+
x_mm: 10.5,
|
|
420
|
+
y_mm: 15.2,
|
|
421
|
+
severity: "error", // "error" | "warning" | "info"
|
|
422
|
+
layer: "top", // "top" | "bottom"
|
|
423
|
+
data: { description: "Via too close to trace" }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Add multiple markers efficiently
|
|
427
|
+
viewer.addMarkers([
|
|
428
|
+
{ id: "error1", x_mm: 20, y_mm: 25, severity: "error" },
|
|
429
|
+
{ id: "warn1", x_mm: 30, y_mm: 35, severity: "warning" },
|
|
430
|
+
{ id: "info1", x_mm: 40, y_mm: 45, severity: "info" }
|
|
431
|
+
]);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Marker Types
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
type Marker = {
|
|
438
|
+
id: string;
|
|
439
|
+
x_mm: number;
|
|
440
|
+
y_mm: number;
|
|
441
|
+
|
|
442
|
+
// Optional metadata
|
|
443
|
+
layer?: "top" | "bottom";
|
|
444
|
+
severity?: "error" | "warning" | "info";
|
|
445
|
+
radius_mm?: number; // For future world-space rendering
|
|
446
|
+
data?: Record<string, any>; // Custom data
|
|
447
|
+
};
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Marker Management
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// Update marker properties
|
|
454
|
+
viewer.updateMarker("test-point-1", {
|
|
455
|
+
severity: "warning",
|
|
456
|
+
x_mm: 11.0 // Position changes update spatial index automatically
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Remove markers
|
|
460
|
+
viewer.removeMarker("test-point-1");
|
|
461
|
+
|
|
462
|
+
// Get marker by ID
|
|
463
|
+
const marker = viewer.getMarker("test-point-1");
|
|
464
|
+
|
|
465
|
+
// List all markers
|
|
466
|
+
const allMarkers = viewer.listMarkers();
|
|
467
|
+
|
|
468
|
+
// Clear all markers
|
|
469
|
+
viewer.clearMarkers();
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Marker Picking and Selection
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
// Pick marker at screen coordinates
|
|
476
|
+
const hit = viewer.pickMarker(mouseX, mouseY, 10); // 10px pick radius
|
|
477
|
+
|
|
478
|
+
if (hit) {
|
|
479
|
+
console.log(`Hit marker: ${hit.id} at ${hit.distance_px.toFixed(1)}px`);
|
|
480
|
+
viewer.selectMarker(hit.id);
|
|
481
|
+
|
|
482
|
+
// Access selected marker
|
|
483
|
+
const selected = viewer.getSelectedMarker();
|
|
484
|
+
console.log("Selected:", selected?.data);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Clear selection
|
|
488
|
+
viewer.selectMarker(null);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Marker Styling
|
|
492
|
+
|
|
493
|
+
Markers are automatically styled by severity:
|
|
494
|
+
- **Error**: Red circles
|
|
495
|
+
- **Warning**: Orange circles
|
|
496
|
+
- **Info**: Blue circles
|
|
497
|
+
- **Default**: Gray circles
|
|
498
|
+
|
|
499
|
+
Selected markers have a blue outline, hovered markers have orange highlighting.
|
|
500
|
+
|
|
501
|
+
### Performance Features
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// The marker system uses:
|
|
505
|
+
// - O(1) lookup via Map storage
|
|
506
|
+
// - Uniform grid spatial index for fast queries
|
|
507
|
+
// - Viewport culling to skip off-screen markers
|
|
508
|
+
// - Constant pixel radius (4px) for visibility at all zooms
|
|
509
|
+
// - Cached list iteration with dirty flag optimization
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Advanced Usage
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// Batch updates for performance
|
|
516
|
+
viewer.updateMarkers([
|
|
517
|
+
{ id: "marker1", severity: "warning" },
|
|
518
|
+
{ id: "marker2", x_mm: 25.0, y_mm: 30.0 }
|
|
519
|
+
]);
|
|
520
|
+
|
|
521
|
+
// Query markers near a point (for custom tools)
|
|
522
|
+
const nearby = viewer.markers.queryNear(20, 20, 5); // 5mm radius
|
|
523
|
+
|
|
524
|
+
// Access underlying systems for advanced integration
|
|
525
|
+
const store = viewer.getMarkerStore();
|
|
526
|
+
const picker = viewer.getMarkerPicker();
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Key Features:**
|
|
530
|
+
- **High Performance**: Spatial index with O(1) lookup
|
|
531
|
+
- **Zoom-Aware Picking**: Accurate hit detection at any zoom level
|
|
532
|
+
- **Automatic Styling**: Severity-based color coding
|
|
533
|
+
- **Interactive Selection**: Click to select, hover for feedback
|
|
534
|
+
- **Batch Operations**: Efficient bulk updates
|
|
535
|
+
- **Custom Data**: Attach any metadata via `data` property
|
|
536
|
+
|
|
537
|
+
## Event System
|
|
538
|
+
|
|
539
|
+
The integrated viewer includes a typed event emitter for responding to user interactions and state changes:
|
|
540
|
+
|
|
541
|
+
### Event Types
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
type ViewerEvents = {
|
|
545
|
+
"hover:marker": { markerId: string | null; marker?: Marker };
|
|
546
|
+
"select:marker": { markerId: string | null; marker?: Marker };
|
|
547
|
+
"click:board": { x_mm: number; y_mm: number };
|
|
548
|
+
"view:change": { center_mm: { x: number; y: number }; zoom: number; rotation_rad: number };
|
|
549
|
+
};
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Setting Up Event Listeners
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
// Set up mouse event handling (call once after viewer creation)
|
|
556
|
+
viewer.setupEventListeners();
|
|
557
|
+
|
|
558
|
+
// Listen to marker hover events
|
|
559
|
+
viewer.on('hover:marker', ({ markerId, marker }) => {
|
|
560
|
+
if (marker) {
|
|
561
|
+
console.log(`Hovering: ${marker.severity} at ${marker.x_mm}, ${marker.y_mm}`);
|
|
562
|
+
// Update tooltip or UI
|
|
563
|
+
} else {
|
|
564
|
+
// Hide tooltip
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Listen to marker selection events
|
|
569
|
+
viewer.on('select:marker', ({ markerId, marker }) => {
|
|
570
|
+
if (marker) {
|
|
571
|
+
console.log(`Selected: ${marker.data?.description}`);
|
|
572
|
+
// Show details panel or highlight related elements
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Listen to board clicks (clicking empty space)
|
|
577
|
+
viewer.on('click:board', ({ x_mm, y_mm }) => {
|
|
578
|
+
console.log(`Clicked board at ${x_mm.toFixed(2)}, ${y_mm.toFixed(2)}mm`);
|
|
579
|
+
// Could add a marker at this position or show context menu
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Listen to view changes (pan/zoom)
|
|
583
|
+
viewer.on('view:change', ({ center_mm, zoom, rotation_rad }) => {
|
|
584
|
+
console.log(`View changed: center=${center_mm.x},${center_mm.y} zoom=${zoom}`);
|
|
585
|
+
// Update coordinates display or save view state
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Event Listener Options
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
// One-time listener (auto-unsubscribes after first event)
|
|
593
|
+
viewer.once('select:marker', ({ marker }) => {
|
|
594
|
+
console.log('First selection:', marker?.id);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Manual unsubscribe
|
|
598
|
+
const unsub = viewer.on('hover:marker', onHover);
|
|
599
|
+
// Later...
|
|
600
|
+
unsub(); // Remove listener
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Event Characteristics
|
|
604
|
+
|
|
605
|
+
- **No Spam**: Events only emit when state actually changes
|
|
606
|
+
- **Type Safe**: Full TypeScript typing for all events
|
|
607
|
+
- **Performance**: Efficient Set-based handler storage
|
|
608
|
+
- **Robust**: Safe unsubscribe even during event emission
|
|
609
|
+
|
|
610
|
+
### Integration with Markers
|
|
611
|
+
|
|
612
|
+
The event system integrates seamlessly with the marker system:
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// Add markers
|
|
616
|
+
viewer.addMarkers([
|
|
617
|
+
{ id: 'error1', x_mm: 10, y_mm: 15, severity: 'error', data: { issue: 'Via too close' } },
|
|
618
|
+
{ id: 'warn1', x_mm: 20, y_mm: 25, severity: 'warning', data: { issue: 'Trace width' } }
|
|
619
|
+
]);
|
|
620
|
+
|
|
621
|
+
// Events will automatically fire for hover/selection
|
|
622
|
+
viewer.setupEventListeners();
|
|
623
|
+
|
|
624
|
+
// Build interactive UI
|
|
625
|
+
viewer.on('hover:marker', ({ marker }) => {
|
|
626
|
+
if (marker?.severity === 'error') {
|
|
627
|
+
showTooltip(marker.data?.issue);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
viewer.on('select:marker', ({ marker }) => {
|
|
632
|
+
if (marker) {
|
|
633
|
+
showDetailsPanel(marker);
|
|
634
|
+
// Optional: center view on selection
|
|
635
|
+
viewer.selectMarker(marker.id, { center: true, animate: true });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Advanced Usage
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// Custom event handling for DFM tools
|
|
644
|
+
class DFMTool {
|
|
645
|
+
constructor(viewer) {
|
|
646
|
+
this.viewer = viewer;
|
|
647
|
+
this.setupEvents();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
setupEvents() {
|
|
651
|
+
this.viewer.on('hover:marker', this.onHover.bind(this));
|
|
652
|
+
this.viewer.on('select:marker', this.onSelect.bind(this));
|
|
653
|
+
this.viewer.on('click:board', this.onBoardClick.bind(this));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
onHover({ marker }) {
|
|
657
|
+
if (marker?.severity === 'error') {
|
|
658
|
+
this.showCriticalError(marker);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
onSelect({ marker }) {
|
|
663
|
+
if (marker) {
|
|
664
|
+
this.zoomToIssue(marker);
|
|
665
|
+
this.showRelatedElements(marker);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
onBoardClick({ x_mm, y_mm }) {
|
|
670
|
+
// Add new marker at click position
|
|
671
|
+
this.viewer.addMarker({
|
|
672
|
+
id: `custom-${Date.now()}`,
|
|
673
|
+
x_mm,
|
|
674
|
+
y_mm,
|
|
675
|
+
severity: 'info',
|
|
676
|
+
data: { source: 'user-click' }
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Usage
|
|
682
|
+
const dfmTool = new DFMTool(viewer);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Key Features:**
|
|
686
|
+
- **Typed Events**: Full TypeScript safety with event payloads
|
|
687
|
+
- **State Change Detection**: No duplicate events spam
|
|
688
|
+
- **Memory Safe**: Automatic cleanup and unsubscribe support
|
|
689
|
+
- **Interactive**: Built-in hover and selection handling
|
|
690
|
+
- **Extensible**: Easy to add custom event handling logic
|
|
691
|
+
|
|
692
|
+
## Visibility System
|
|
693
|
+
|
|
694
|
+
The integrated viewer includes a centralized visibility management system that controls which layers and features are rendered:
|
|
695
|
+
|
|
696
|
+
### Visibility State Structure
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
type VisibilityState = {
|
|
700
|
+
gerber: {
|
|
701
|
+
copper: boolean; // Copper traces
|
|
702
|
+
solderMask: boolean; // Solder mask layers
|
|
703
|
+
silk: boolean; // Silk screen layers
|
|
704
|
+
outline: boolean; // Board outline
|
|
705
|
+
};
|
|
706
|
+
overlays: Record<string, boolean>; // Custom overlay visibility
|
|
707
|
+
markers: boolean; // Marker system visibility
|
|
708
|
+
};
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Basic Visibility Control
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
// Get current visibility state
|
|
715
|
+
const state = viewer.getVisibility();
|
|
716
|
+
console.log(state.gerber.copper); // true/false
|
|
717
|
+
console.log(state.markers); // true/false
|
|
718
|
+
|
|
719
|
+
// Set individual layer visibility
|
|
720
|
+
viewer.setGerberVisibility('copper', false);
|
|
721
|
+
viewer.setOverlayVisibility('grid', true);
|
|
722
|
+
viewer.setMarkersVisibility(true);
|
|
723
|
+
|
|
724
|
+
// Toggle layers
|
|
725
|
+
viewer.toggleGerberLayer('silk');
|
|
726
|
+
viewer.toggleOverlay('grid');
|
|
727
|
+
viewer.toggleMarkers();
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Visibility Presets
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// Quick visibility presets for common use cases
|
|
734
|
+
viewer.applyVisibilityPreset('all'); // Show everything
|
|
735
|
+
viewer.applyVisibilityPreset('none'); // Hide everything
|
|
736
|
+
viewer.applyVisibilityPreset('copper-only'); // Only copper + outline
|
|
737
|
+
viewer.applyVisibilityPreset('minimal'); // Copper + outline + markers
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Reactive Updates
|
|
741
|
+
|
|
742
|
+
The visibility system supports reactive updates through subscriptions:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// Subscribe to visibility changes
|
|
746
|
+
const unsubscribe = viewer.onVisibilityChange((state) => {
|
|
747
|
+
console.log('Visibility changed:', state);
|
|
748
|
+
// Update UI controls, save state, etc.
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Later, when done
|
|
752
|
+
unsubscribe();
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Integration with Render Pipeline
|
|
756
|
+
|
|
757
|
+
All render passes receive the current visibility state and can make rendering decisions based on it:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Gerber layers check their specific visibility
|
|
761
|
+
enabled: (rc) => rc.visibility.gerber.copper,
|
|
762
|
+
|
|
763
|
+
// Markers check global marker visibility
|
|
764
|
+
enabled: (rc) => rc.visibility.markers,
|
|
765
|
+
|
|
766
|
+
// Overlays filter by their individual visibility
|
|
767
|
+
const visibleOverlays = overlays.filter(overlay =>
|
|
768
|
+
rc.visibility.overlays[overlay.id] ?? overlay.visible
|
|
769
|
+
);
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Advanced Usage
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
// Custom visibility management
|
|
776
|
+
class CustomUI {
|
|
777
|
+
constructor(viewer) {
|
|
778
|
+
this.viewer = viewer;
|
|
779
|
+
this.setupControls();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
setupControls() {
|
|
783
|
+
// Create custom visibility controls
|
|
784
|
+
this.createLayerToggles();
|
|
785
|
+
this.setupPresets();
|
|
786
|
+
this.setupReactiveUI();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
createLayerToggles() {
|
|
790
|
+
const layers = ['copper', 'solderMask', 'silk', 'outline'];
|
|
791
|
+
|
|
792
|
+
layers.forEach(layer => {
|
|
793
|
+
const toggle = document.createElement('input');
|
|
794
|
+
toggle.type = 'checkbox';
|
|
795
|
+
toggle.checked = this.viewer.getVisibility().gerber[layer];
|
|
796
|
+
|
|
797
|
+
toggle.addEventListener('change', () => {
|
|
798
|
+
this.viewer.setGerberVisibility(layer, toggle.checked);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
document.body.appendChild(toggle);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
setupPresets() {
|
|
806
|
+
const presetSelect = document.createElement('select');
|
|
807
|
+
|
|
808
|
+
const presets = [
|
|
809
|
+
{ value: 'all', label: 'All Layers' },
|
|
810
|
+
{ value: 'none', label: 'None' },
|
|
811
|
+
{ value: 'copper-only', label: 'Copper Only' },
|
|
812
|
+
{ value: 'minimal', label: 'Minimal' }
|
|
813
|
+
];
|
|
814
|
+
|
|
815
|
+
presets.forEach(preset => {
|
|
816
|
+
const option = document.createElement('option');
|
|
817
|
+
option.value = preset.value;
|
|
818
|
+
option.textContent = preset.label;
|
|
819
|
+
presetSelect.appendChild(option);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
presetSelect.addEventListener('change', () => {
|
|
823
|
+
this.viewer.applyVisibilityPreset(presetSelect.value);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
document.body.appendChild(presetSelect);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
setupReactiveUI() {
|
|
830
|
+
// Update UI when visibility changes
|
|
831
|
+
this.viewer.onVisibilityChange((state) => {
|
|
832
|
+
// Update checkbox states
|
|
833
|
+
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
|
834
|
+
const layer = checkbox.dataset.layer;
|
|
835
|
+
if (layer && state.gerber[layer]) {
|
|
836
|
+
checkbox.checked = true;
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Update status display
|
|
841
|
+
const visibleCount = Object.values(state.gerber).filter(Boolean).length;
|
|
842
|
+
console.log(`Visible layers: ${visibleCount}/4`);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Usage
|
|
848
|
+
const customUI = new CustomUI(viewer);
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Performance Features
|
|
852
|
+
|
|
853
|
+
- **Centralized State**: Single source of truth prevents inconsistencies
|
|
854
|
+
- **Efficient Updates**: Only re-renders when visibility actually changes
|
|
855
|
+
- **Subscription System**: Reactive updates without polling
|
|
856
|
+
- **Type Safety**: Full TypeScript support prevents runtime errors
|
|
857
|
+
- **Batch Operations**: Presets and bulk updates minimize renders
|
|
858
|
+
|
|
859
|
+
### Integration Examples
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
// DFM tool with custom visibility controls
|
|
863
|
+
class DFMTool {
|
|
864
|
+
constructor(viewer) {
|
|
865
|
+
this.viewer = viewer;
|
|
866
|
+
this.setupDFMPresets();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
setupDFMPresets() {
|
|
870
|
+
// Custom DFM-specific presets
|
|
871
|
+
this.addPreset('dfm-errors', {
|
|
872
|
+
gerber: { copper: true, solderMask: false, silk: false, outline: true },
|
|
873
|
+
markers: true,
|
|
874
|
+
overlays: { 'dfm-highlights': true }
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
this.addPreset('manufacturing', {
|
|
878
|
+
gerber: { copper: true, solderMask: true, silk: true, outline: true },
|
|
879
|
+
markers: false,
|
|
880
|
+
overlays: { 'dimensions': true, 'tolerances': true }
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
addPreset(name, config) {
|
|
885
|
+
// Add custom preset button
|
|
886
|
+
const button = document.createElement('button');
|
|
887
|
+
button.textContent = name;
|
|
888
|
+
button.addEventListener('click', () => {
|
|
889
|
+
this.viewer.setVisibility(config);
|
|
890
|
+
});
|
|
891
|
+
document.body.appendChild(button);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
**Key Features:**
|
|
897
|
+
- **Centralized Management**: Single visibility state prevents inconsistencies
|
|
898
|
+
- **Reactive Updates**: Automatic re-renders on state changes
|
|
899
|
+
- **Type Safe**: Full TypeScript support throughout
|
|
900
|
+
- **Performance Optimized**: Efficient subscription-based updates
|
|
901
|
+
- **Extensible**: Easy to add custom controls and presets
|
|
902
|
+
|
|
903
|
+
### Render Pipeline
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
import { createIntegratedViewer } from "gerbers-renderer";
|
|
907
|
+
|
|
908
|
+
const viewer = createIntegratedViewer(container);
|
|
909
|
+
viewer.setData({ boardGeom, layers });
|
|
910
|
+
|
|
911
|
+
// Add custom render passes
|
|
912
|
+
viewer.viewer.addPass({
|
|
913
|
+
id: "custom-overlay",
|
|
914
|
+
order: 150,
|
|
915
|
+
enabled: () => true,
|
|
916
|
+
draw: (rc) => {
|
|
917
|
+
// Draw in board coordinates
|
|
918
|
+
const m = rc.xform.getWorldToScreenMatrix();
|
|
919
|
+
rc.ctx.setTransform(m[0], m[3], m[1], m[4], m[2], m[5]);
|
|
920
|
+
// ... your drawing code
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Render Stages:**
|
|
926
|
+
- **Base Gerber** (0-99): Copper traces, masks, silk screen
|
|
927
|
+
- **Overlays** (100-199): Grid, rulers, custom drawings
|
|
928
|
+
- **Markers** (200-299): Test points, components, annotations
|
|
929
|
+
- **Selection** (300-399): Highlighted regions and elements
|
|
930
|
+
|
|
931
|
+
**Key Features:**
|
|
932
|
+
- Deterministic rendering order
|
|
933
|
+
- Efficient render scheduling with requestAnimationFrame
|
|
934
|
+
- Centralized visibility management
|
|
935
|
+
- Extensible render pass system
|
|
936
|
+
|
|
937
|
+
**Visibility Control:**
|
|
938
|
+
```typescript
|
|
939
|
+
// Use presets
|
|
940
|
+
viewer.visibility.applyPreset('copper-only');
|
|
941
|
+
|
|
942
|
+
// Individual control
|
|
943
|
+
viewer.visibility.setGerberVisibility('copper', false);
|
|
944
|
+
viewer.visibility.setOverlayVisibility('grid', true);
|
|
945
|
+
viewer.visibility.setMarkersVisibility(true);
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**File Organization:**
|
|
949
|
+
- `src/render-pipeline/core/`: Core components (transforms, contracts, scheduling)
|
|
950
|
+
- `src/render-pipeline/`: Complete render pipeline implementation
|
|
951
|
+
- `src/render-pipeline/overlayRegistry.ts`: Overlay management system
|
|
952
|
+
- `src/render-pipeline/exampleOverlays.ts`: Built-in overlay examples
|
|
953
|
+
- `src/viewer/`: Shared types and styles
|
|
954
|
+
- `src/index.ts`: Unified exports
|
|
955
|
+
|
|
956
|
+
See [FILE_STRUCTURE.md](./FILE_STRUCTURE.md) for detailed organization.
|
|
957
|
+
|
|
241
958
|
## License
|
|
242
959
|
|
|
243
960
|
MIT
|