Skip to content

Phase 5: Advanced Features & Professional Tools - PCB Viewer Frontend #445

Description

@nam20485

Phase 5: Advanced Features & Professional Tools

Parent Issue: #440
Duration: 4-5 days
Goal: Implement advanced professional-grade features that distinguish this from basic PCB viewers.

Overview

This phase adds sophisticated features typically found in professional PCB analysis tools, including layer stack management, 3D visualization, design rule checking, cross-probing, and advanced analysis capabilities. These features provide the professional polish that makes the viewer suitable for serious PCB work.

Detailed Development Steps

5.1 Layer Stack Management

  • Layer Stack Visualization

    // src/components/pcb/LayerStack/LayerStackPanel.tsx
    interface LayerStackPanelProps {
      layers: TscircuitLayer[];
      activeLayer: string | null;
      visibleLayers: string[];
      onLayerToggle: (layerId: string) => void;
      onLayerSelect: (layerId: string) => void;
      onLayerOpacityChange: (layerId: string, opacity: number) => void;
    }
    
    export const LayerStackPanel: React.FC<LayerStackPanelProps> = ({
      layers,
      activeLayer,
      visibleLayers,
      onLayerToggle,
      onLayerSelect,
      onLayerOpacityChange
    }) => {
      const [layerSettings, setLayerSettings] = useState<Record<string, LayerSettings>>({});
      
      const layerTypes = useMemo(() => {
        return layers.reduce((types, layer) => {
          const type = LayerTypeDetector.getLayerType(layer.name);
          if (!types[type]) types[type] = [];
          types[type].push(layer);
          return types;
        }, {} as Record<LayerType, TscircuitLayer[]>);
      }, [layers]);
      
      return (
        <div className="layer-stack-panel">
          <div className="layer-stack-header">
            <h3>Layer Stack</h3>
            <div className="layer-controls">
              <button 
                onClick={() => onToggleAllLayers()}
                title="Toggle All Layers"
              >
                <Layers size={16} />
              </button>
              <button 
                onClick={() => resetLayerSettings()}
                title="Reset Layer Settings"
              >
                <RotateCcw size={16} />
              </button>
            </div>
          </div>
          
          <div className="layer-stack-content">
            {Object.entries(layerTypes).map(([type, typeLayers]) => (
              <div key={type} className="layer-type-group">
                <div className="layer-type-header">
                  <span className="layer-type-name">{formatLayerTypeName(type)}</span>
                  <span className="layer-count">({typeLayers.length})</span>
                </div>
                
                <div className="layer-type-layers">
                  {typeLayers.map(layer => (
                    <LayerStackItem
                      key={layer.name}
                      layer={layer}
                      isActive={activeLayer === layer.name}
                      isVisible={visibleLayers.includes(layer.name)}
                      settings={layerSettings[layer.name]}
                      onToggle={() => onLayerToggle(layer.name)}
                      onSelect={() => onLayerSelect(layer.name)}
                      onSettingsChange={(settings) => setLayerSettings(prev => ({
                        ...prev,
                        [layer.name]: settings
                      }))}
                    />
                  ))}
                </div>
              </div>
            ))}
          </div>
        </div>
      );
    };
  • Layer Configuration Manager

    // src/services/pcb/layerManager.ts
    export class LayerManager {
      private layers: Map<string, TscircuitLayer> = new Map();
      private layerSettings: Map<string, LayerSettings> = new Map();
      private renderOrder: string[] = [];
      
      constructor(layers: TscircuitLayer[]) {
        this.initializeLayers(layers);
      }
      
      private initializeLayers(layers: TscircuitLayer[]) {
        // Group layers by type and establish render order
        const layersByType = this.groupLayersByType(layers);
        
        // Standard PCB layer ordering (bottom to top)
        const standardOrder = [
          LayerType.BOTTOM_COPPER,
          LayerType.BOTTOM_SOLDER_MASK,
          LayerType.BOTTOM_SILK_SCREEN,
          LayerType.INNER_COPPER,
          LayerType.DIELECTRIC,
          LayerType.TOP_COPPER,
          LayerType.TOP_SOLDER_MASK,
          LayerType.TOP_SILK_SCREEN,
          LayerType.DRILL,
          LayerType.MECHANICAL,
          LayerType.ASSEMBLY
        ];
        
        // Build render order based on standard ordering
        this.renderOrder = [];
        for (const type of standardOrder) {
          if (layersByType[type]) {
            this.renderOrder.push(...layersByType[type].map(l => l.name));
          }
        }
        
        // Initialize layer settings with defaults
        for (const layer of layers) {
          this.layers.set(layer.name, layer);
          this.layerSettings.set(layer.name, this.getDefaultLayerSettings(layer));
        }
      }
      
      getDefaultLayerSettings(layer: TscircuitLayer): LayerSettings {
        const layerType = LayerTypeDetector.getLayerType(layer.name);
        
        return {
          visible: this.getDefaultVisibility(layerType),
          opacity: this.getDefaultOpacity(layerType),
          color: this.getDefaultColor(layerType),
          renderStyle: this.getDefaultRenderStyle(layerType)
        };
      }
      
      getLayersInRenderOrder(): TscircuitLayer[] {
        return this.renderOrder
          .map(name => this.layers.get(name)!)
          .filter(layer => this.isLayerVisible(layer.name));
      }
      
      setLayerVisibility(layerName: string, visible: boolean): void {
        const settings = this.layerSettings.get(layerName);
        if (settings) {
          this.layerSettings.set(layerName, { ...settings, visible });
        }
      }
      
      setLayerOpacity(layerName: string, opacity: number): void {
        const settings = this.layerSettings.get(layerName);
        if (settings) {
          this.layerSettings.set(layerName, { ...settings, opacity });
        }
      }
      
      // Advanced layer filtering
      showOnlyLayer(layerName: string): void {
        for (const [name, settings] of this.layerSettings) {
          this.layerSettings.set(name, { 
            ...settings, 
            visible: name === layerName 
          });
        }
      }
      
      showLayerType(layerType: LayerType): void {
        for (const [name, layer] of this.layers) {
          const settings = this.layerSettings.get(name)!;
          const isTargetType = LayerTypeDetector.getLayerType(layer.name) === layerType;
          this.layerSettings.set(name, { 
            ...settings, 
            visible: isTargetType 
          });
        }
      }
    }

5.2 3D Visualization Mode

  • 3D Board Representation

    // src/components/pcb/3D/Board3DViewer.tsx
    import { Canvas, useFrame, useThree } from '@react-three/fiber';
    import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
    
    interface Board3DViewerProps {
      pcbData: TscircuitPcbData;
      layerStack: TscircuitLayer[];
      components: TscircuitComponent[];
      viewSettings: View3DSettings;
    }
    
    export const Board3DViewer: React.FC<Board3DViewerProps> = ({
      pcbData,
      layerStack,
      components,
      viewSettings
    }) => {
      const [meshes, setMeshes] = useState<THREE.Mesh[]>([]);
      
      useEffect(() => {
        const builder = new PCB3DBuilder(pcbData, layerStack, components);
        const generated3D = builder.generateBoardMesh(viewSettings);
        setMeshes(generated3D.meshes);
      }, [pcbData, layerStack, components, viewSettings]);
      
      return (
        <div className="board-3d-viewer">
          <Canvas>
            <PerspectiveCamera 
              makeDefault 
              position={[0, 0, 10]} 
              fov={50}
            />
            
            <OrbitControls 
              enableDamping
              dampingFactor={0.05}
              screenSpacePanning={false}
              minDistance={1}
              maxDistance={100}
            />
            
            <ambientLight intensity={0.4} />
            <directionalLight 
              position={[10, 10, 5]} 
              intensity={0.8}
              castShadow
            />
            <directionalLight 
              position={[-10, -10, -5]} 
              intensity={0.3}
            />
            
            <PCBBoard3D 
              layers={layerStack}
              settings={viewSettings}
            />
            
            <Components3D 
              components={components}
              settings={viewSettings}
            />
            
            {viewSettings.showShadows && (
              <mesh 
                receiveShadow 
                rotation={[-Math.PI / 2, 0, 0]} 
                position={[0, -2, 0]}
              >
                <planeGeometry args={[100, 100]} />
                <shadowMaterial opacity={0.3} />
              </mesh>
            )}
          </Canvas>
          
          <View3DControls 
            settings={viewSettings}
            onSettingsChange={onSettingsChange}
          />
        </div>
      );
    };
  • Component 3D Models

    // src/services/pcb/3d/component3DLibrary.ts
    export class Component3DLibrary {
      private static modelCache = new Map<string, THREE.Group>();
      
      static async getComponentModel(
        component: TscircuitComponent
      ): Promise<THREE.Group> {
        const modelKey = this.getModelKey(component);
        
        if (this.modelCache.has(modelKey)) {
          return this.modelCache.get(modelKey)!.clone();
        }
        
        const model = await this.generateComponentModel(component);
        this.modelCache.set(modelKey, model);
        
        return model.clone();
      }
      
      private static async generateComponentModel(
        component: TscircuitComponent
      ): Promise<THREE.Group> {
        const group = new THREE.Group();
        
        // Generate component body
        const body = this.createComponentBody(component);
        group.add(body);
        
        // Add pins/pads
        const pins = this.createComponentPins(component);
        group.add(...pins);
        
        // Add component marking/labels
        if (component.marking) {
          const label = this.createComponentLabel(component.marking);
          group.add(label);
        }
        
        return group;
      }
      
      private static createComponentBody(component: TscircuitComponent): THREE.Mesh {
        const footprint = component.footprint;
        const packageType = this.detectPackageType(footprint);
        
        switch (packageType) {
          case 'BGA':
            return this.createBGABody(component);
          case 'QFP':
            return this.createQFPBody(component);
          case 'SOIC':
            return this.createSOICBody(component);
          case 'Resistor':
            return this.createResistorBody(component);
          case 'Capacitor':
            return this.createCapacitorBody(component);
          default:
            return this.createGenericBody(component);
        }
      }
      
      private static createBGABody(component: TscircuitComponent): THREE.Mesh {
        const { width, height, thickness } = this.getComponentDimensions(component);
        
        const geometry = new THREE.BoxGeometry(width, height, thickness);
        const material = new THREE.MeshPhongMaterial({
          color: 0x2d2d2d,
          shininess: 30
        });
        
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.z = thickness / 2;
        
        return mesh;
      }
      
      private static createQFPBody(component: TscircuitComponent): THREE.Mesh {
        const { width, height, thickness } = this.getComponentDimensions(component);
        
        // Create main body
        const bodyGeometry = new THREE.BoxGeometry(width * 0.8, height * 0.8, thickness);
        const bodyMaterial = new THREE.MeshPhongMaterial({
          color: 0x1a1a1a,
          shininess: 50
        });
        
        const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
        body.position.z = thickness / 2;
        
        return body;
      }
    }

5.3 Design Rule Checking (DRC)

  • Basic DRC Engine

    // src/services/pcb/drc/drcEngine.ts
    export class DRCEngine {
      private rules: DRCRule[];
      private violations: DRCViolation[] = [];
      
      constructor(rules: DRCRule[]) {
        this.rules = rules;
      }
      
      async checkDesign(
        pcbData: TscircuitPcbData,
        components: TscircuitComponent[],
        nets: TscircuitNet[]
      ): Promise<DRCReport> {
        this.violations = [];
        
        // Run all DRC checks
        await Promise.all([
          this.checkMinimumSpacing(pcbData),
          this.checkViaRules(pcbData),
          this.checkTraceWidth(nets),
          this.checkComponentPlacement(components),
          this.checkDrillSizes(pcbData),
          this.checkAnnularRings(pcbData),
          this.checkCopperPours(pcbData)
        ]);
        
        return this.generateReport();
      }
      
      private async checkMinimumSpacing(pcbData: TscircuitPcbData): Promise<void> {
        const spacingRule = this.rules.find(r => r.type === 'minimum_spacing') as MinimumSpacingRule;
        if (!spacingRule) return;
        
        const features = pcbData.getAllCopperFeatures();
        
        for (let i = 0; i < features.length; i++) {
          for (let j = i + 1; j < features.length; j++) {
            const distance = GeometryUtils.calculateDistance(features[i], features[j]);
            
            if (distance < spacingRule.minimumDistance) {
              this.violations.push({
                id: generateId(),
                type: 'spacing_violation',
                severity: 'error',
                message: `Minimum spacing violation: ${distance.toFixed(3)}mm < ${spacingRule.minimumDistance}mm`,
                location: GeometryUtils.getMidpoint(features[i], features[j]),
                affectedFeatures: [features[i].id, features[j].id]
              });
            }
          }
        }
      }
      
      private async checkTraceWidth(nets: TscircuitNet[]): Promise<void> {
        const widthRule = this.rules.find(r => r.type === 'minimum_trace_width') as TraceWidthRule;
        if (!widthRule) return;
        
        for (const net of nets) {
          for (const segment of net.traces) {
            if (segment.width < widthRule.minimumWidth) {
              this.violations.push({
                id: generateId(),
                type: 'trace_width_violation',
                severity: 'error',
                message: `Trace width ${segment.width}mm below minimum ${widthRule.minimumWidth}mm`,
                location: segment.midpoint,
                affectedFeatures: [segment.id],
                netName: net.name
              });
            }
          }
        }
      }
      
      private async checkComponentPlacement(components: TscircuitComponent[]): Promise<void> {
        const placementRule = this.rules.find(r => r.type === 'component_spacing') as ComponentSpacingRule;
        if (!placementRule) return;
        
        for (let i = 0; i < components.length; i++) {
          for (let j = i + 1; j < components.length; j++) {
            const comp1 = components[i];
            const comp2 = components[j];
            
            // Skip if components are on different layers
            if (comp1.layer !== comp2.layer) continue;
            
            const distance = GeometryUtils.calculateComponentDistance(comp1, comp2);
            const requiredSpacing = this.getRequiredSpacing(comp1, comp2, placementRule);
            
            if (distance < requiredSpacing) {
              this.violations.push({
                id: generateId(),
                type: 'component_spacing_violation',
                severity: 'warning',
                message: `Components too close: ${distance.toFixed(3)}mm < ${requiredSpacing}mm`,
                location: GeometryUtils.getMidpoint(comp1.position, comp2.position),
                affectedFeatures: [comp1.id, comp2.id]
              });
            }
          }
        }
      }
      
      generateReport(): DRCReport {
        const errorCount = this.violations.filter(v => v.severity === 'error').length;
        const warningCount = this.violations.filter(v => v.severity === 'warning').length;
        
        return {
          timestamp: new Date(),
          totalViolations: this.violations.length,
          errorCount,
          warningCount,
          violations: this.violations,
          summary: this.generateSummary(),
          recommendations: this.generateRecommendations()
        };
      }
    }
  • DRC Results Viewer

    // src/components/pcb/DRC/DRCResultsPanel.tsx
    interface DRCResultsPanelProps {
      report: DRCReport | null;
      onViolationClick: (violation: DRCViolation) => void;
      onFixViolation: (violationId: string) => void;
    }
    
    export const DRCResultsPanel: React.FC<DRCResultsPanelProps> = ({
      report,
      onViolationClick,
      onFixViolation
    }) => {
      const [filter, setFilter] = useState<DRCViolationSeverity | 'all'>('all');
      const [groupBy, setGroupBy] = useState<'type' | 'severity' | 'layer'>('type');
      
      const filteredViolations = useMemo(() => {
        if (!report) return [];
        
        let violations = report.violations;
        
        if (filter !== 'all') {
          violations = violations.filter(v => v.severity === filter);
        }
        
        return violations;
      }, [report, filter]);
      
      const groupedViolations = useMemo(() => {
        return filteredViolations.reduce((groups, violation) => {
          const key = violation[groupBy] || 'Other';
          if (!groups[key]) groups[key] = [];
          groups[key].push(violation);
          return groups;
        }, {} as Record<string, DRCViolation[]>);
      }, [filteredViolations, groupBy]);
      
      if (!report) {
        return (
          <div className="drc-results-panel">
            <div className="no-results">
              <AlertTriangle size={48} />
              <p>No DRC report available</p>
              <p>Run DRC check to see results</p>
            </div>
          </div>
        );
      }
      
      return (
        <div className="drc-results-panel">
          <div className="drc-header">
            <h3>DRC Results</h3>
            <div className="drc-summary">
              <span className={`violation-count errors ${report.errorCount === 0 ? 'success' : ''}`}>
                {report.errorCount} Errors
              </span>
              <span className={`violation-count warnings ${report.warningCount === 0 ? 'success' : ''}`}>
                {report.warningCount} Warnings
              </span>
            </div>
          </div>
          
          <div className="drc-controls">
            <select 
              value={filter} 
              onChange={(e) => setFilter(e.target.value as any)}
            >
              <option value="all">All Violations</option>
              <option value="error">Errors Only</option>
              <option value="warning">Warnings Only</option>
            </select>
            
            <select 
              value={groupBy} 
              onChange={(e) => setGroupBy(e.target.value as any)}
            >
              <option value="type">Group by Type</option>
              <option value="severity">Group by Severity</option>
              <option value="layer">Group by Layer</option>
            </select>
          </div>
          
          <div className="drc-violations">
            {Object.entries(groupedViolations).map(([group, violations]) => (
              <div key={group} className="violation-group">
                <div className="group-header">
                  <span className="group-name">{group}</span>
                  <span className="group-count">({violations.length})</span>
                </div>
                
                <div className="group-violations">
                  {violations.map(violation => (
                    <DRCViolationItem
                      key={violation.id}
                      violation={violation}
                      onClick={() => onViolationClick(violation)}
                      onFix={() => onFixViolation(violation.id)}
                    />
                  ))}
                </div>
              </div>
            ))}
          </div>
        </div>
      );
    };

5.4 Cross-Probing System

  • Cross-Reference Manager
    // src/services/pcb/crossProbing.ts
    export class CrossProbingManager {
      private componentNetMap = new Map<string, string[]>();
      private netComponentMap = new Map<string, string[]>();
      private listeners: CrossProbingListener[] = [];
      
      constructor(components: TscircuitComponent[], nets: TscircuitNet[]) {
        this.buildCrossReferenceMaps(components, nets);
      }
      
      private buildCrossReferenceMaps(
        components: TscircuitComponent[], 
        nets: TscircuitNet[]
      ): void {
        // Build component -> nets mapping
        for (const component of components) {
          const connectedNets = nets
            .filter(net => 
              net.connections.some(conn => conn.component === component.id)
            )
            .map(net => net.name);
          
          this.componentNetMap.set(component.id, connectedNets);
        }
        
        // Build net -> components mapping
        for (const net of nets) {
          const connectedComponents = net.connections.map(conn => conn.component);
          this.netComponentMap.set(net.name, connectedComponents);
        }
      }
      
      probeComponent(componentId: string): CrossProbingResult {
        const connectedNets = this.componentNetMap.get(componentId) || [];
        const relatedComponents = new Set<string>();
        
        // Find all components connected to the same nets
        for (const netName of connectedNets) {
          const netComponents = this.netComponentMap.get(netName) || [];
          netComponents.forEach(compId => {
            if (compId !== componentId) {
              relatedComponents.add(compId);
            }
          });
        }
        
        const result: CrossProbingResult = {
          sourceType: 'component',
          sourceId: componentId,
          connectedNets,
          relatedComponents: Array.from(relatedComponents),
          highlightAreas: this.calculateHighlightAreas(componentId, connectedNets)
        };
        
        this.notifyListeners('component_probed', result);
        return result;
      }
      
      probeNet(netName: string): CrossProbingResult {
        const connectedComponents = this.netComponentMap.get(netName) || [];
        const relatedNets = this.findRelatedNets(netName, connectedComponents);
        
        const result: CrossProbingResult = {
          sourceType: 'net',
          sourceId: netName,
          connectedNets: [netName, ...relatedNets],
          relatedComponents: connectedComponents,
          highlightAreas: this.calculateNetHighlightAreas(netName)
        };
        
        this.notifyListeners('net_probed', result);
        return result;
      }
      
      probePad(componentId: string, padName: string): CrossProbingResult {
        // Find the specific net connected to this pad
        const component = this.findComponent(componentId);
        const connectedNet = this.findNetForPad(componentId, padName);
        
        if (!connectedNet) {
          return this.createEmptyResult('pad', `${componentId}.${padName}`);
        }
        
        const result: CrossProbingResult = {
          sourceType: 'pad',
          sourceId: `${componentId}.${padName}`,
          connectedNets: [connectedNet],
          relatedComponents: this.netComponentMap.get(connectedNet) || [],
          highlightAreas: this.calculatePadHighlightAreas(componentId, padName, connectedNet)
        };
        
        this.notifyListeners('pad_probed', result);
        return result;
      }
      
      // Advanced cross-probing: find signal paths
      findSignalPath(
        sourceComponent: string, 
        targetComponent: string
      ): SignalPath[] {
        const paths: SignalPath[] = [];
        const sourceNets = this.componentNetMap.get(sourceComponent) || [];
        const targetNets = this.componentNetMap.get(targetComponent) || [];
        
        // Find direct connections
        for (const net of sourceNets) {
          if (targetNets.includes(net)) {
            paths.push({
              type: 'direct',
              sourceComponent,
              targetComponent,
              viaNet: net,
              length: this.calculateNetLength(net),
              layerTransitions: this.getNetLayerTransitions(net)
            });
          }
        }
        
        // Find indirect connections through components
        // (e.g., through resistors, capacitors, connectors)
        // This would require more complex graph traversal
        
        return paths;
      }
      
      addListener(listener: CrossProbingListener): void {
        this.listeners.push(listener);
      }
      
      removeListener(listener: CrossProbingListener): void {
        const index = this.listeners.indexOf(listener);
        if (index > -1) {
          this.listeners.splice(index, 1);
        }
      }
      
      private notifyListeners(event: CrossProbingEvent, result: CrossProbingResult): void {
        for (const listener of this.listeners) {
          listener(event, result);
        }
      }
    }

5.5 Electrical Analysis Tools

  • Connectivity Analyzer
    // src/services/pcb/analysis/connectivityAnalyzer.ts
    export class ConnectivityAnalyzer {
      static analyzeConnectivity(
        components: TscircuitComponent[],
        nets: TscircuitNet[]
      ): ConnectivityReport {
        const report: ConnectivityReport = {
          totalNets: nets.length,
          totalConnections: 0,
          unconnectedPins: [],
          openNets: [],
          shortCircuits: [],
          isolatedComponents: [],
          powerAnalysis: null,
          signalIntegrity: null
        };
        
        // Analyze each net
        for (const net of nets) {
          report.totalConnections += net.connections.length;
          
          // Check for open nets (single connection)
          if (net.connections.length === 1) {
            report.openNets.push({
              netName: net.name,
              connectedPin: net.connections[0]
            });
          }
          
          // Check for potential short circuits
          const shortCheck = this.checkForShorts(net, components);
          if (shortCheck.isShort) {
            report.shortCircuits.push(shortCheck);
          }
        }
        
        // Find unconnected pins
        report.unconnectedPins = this.findUnconnectedPins(components, nets);
        
        // Find isolated components
        report.isolatedComponents = this.findIsolatedComponents(components, nets);
        
        // Power analysis
        report.powerAnalysis = this.analyzePowerDistribution(components, nets);
        
        return report;
      }
      
      private static findUnconnectedPins(
        components: TscircuitComponent[],
        nets: TscircuitNet[]
      ): UnconnectedPin[] {
        const connectedPins = new Set<string>();
        
        // Build set of all connected pins
        for (const net of nets) {
          for (const connection of net.connections) {
            connectedPins.add(`${connection.component}.${connection.pin}`);
          }
        }
        
        const unconnectedPins: UnconnectedPin[] = [];
        
        // Check each component's pins
        for (const component of components) {
          if (!component.footprint?.pins) continue;
          
          for (const pin of component.footprint.pins) {
            const pinKey = `${component.id}.${pin.name}`;
            if (!connectedPins.has(pinKey)) {
              unconnectedPins.push({
                component: component.id,
                componentDesignator: component.designator,
                pin: pin.name,
                pinType: pin.type,
                location: {
                  x: component.position.x + pin.x,
                  y: component.position.y + pin.y
                }
              });
            }
          }
        }
        
        return unconnectedPins;
      }
      
      private static analyzePowerDistribution(
        components: TscircuitComponent[],
        nets: TscircuitNet[]
      ): PowerAnalysis {
        const powerNets = nets.filter(net => 
          this.isPowerNet(net.name) || this.isGroundNet(net.name)
        );
        
        const powerDomains: PowerDomain[] = [];
        
        for (const net of powerNets) {
          const domain: PowerDomain = {
            netName: net.name,
            voltage: this.estimateVoltage(net.name),
            connectedComponents: net.connections.map(c => c.component),
            totalCurrent: this.estimateTotalCurrent(net, components),
            powerPlanes: this.findPowerPlanes(net),
            viaCount: this.countPowerVias(net),
            analysis: this.analyzePowerDomain(net, components)
          };
          
          powerDomains.push(domain);
        }
        
        return {
          powerDomains,
          totalPowerNets: powerNets.length,
          powerIntegrity: this.assessPowerIntegrity(powerDomains),
          recommendations: this.generatePowerRecommendations(powerDomains)
        };
      }
      
      static generateConnectivityReport(analysis: ConnectivityReport): string {
        let report = "PCB Connectivity Analysis Report\\n";
        report += "=====================================\\n\\n";
        
        report += `Total Nets: ${analysis.totalNets}\\n`;
        report += `Total Connections: ${analysis.totalConnections}\\n\\n`;
        
        if (analysis.unconnectedPins.length > 0) {
          report += `Unconnected Pins (${analysis.unconnectedPins.length}):\\n`;
          for (const pin of analysis.unconnectedPins.slice(0, 10)) {
            report += `  - ${pin.componentDesignator}.${pin.pin}\\n`;
          }
          if (analysis.unconnectedPins.length > 10) {
            report += `  ... and ${analysis.unconnectedPins.length - 10} more\\n`;
          }
          report += "\\n";
        }
        
        if (analysis.openNets.length > 0) {
          report += `Open Nets (${analysis.openNets.length}):\\n`;
          for (const openNet of analysis.openNets) {
            report += `  - ${openNet.netName}\\n`;
          }
          report += "\\n";
        }
        
        if (analysis.shortCircuits.length > 0) {
          report += `Potential Short Circuits (${analysis.shortCircuits.length}):\\n`;
          for (const short of analysis.shortCircuits) {
            report += `  - ${short.description}\\n`;
          }
          report += "\\n";
        }
        
        return report;
      }
    }

5.6 Gerber Export Preview

  • Gerber Layer Generator
    // src/services/pcb/export/gerberGenerator.ts
    export class GerberPreviewGenerator {
      static generateGerberPreview(
        pcbData: TscircuitPcbData,
        layers: TscircuitLayer[],
        options: GerberOptions
      ): GerberPreview {
        const gerberLayers: GerberLayer[] = [];
        
        for (const layer of layers) {
          const gerberLayer = this.generateLayerGerber(layer, pcbData, options);
          gerberLayers.push(gerberLayer);
        }
        
        return {
          layers: gerberLayers,
          drillData: this.generateDrillData(pcbData, options),
          apertures: this.generateApertureList(pcbData),
          boardOutline: this.generateBoardOutline(pcbData),
          metadata: this.generateGerberMetadata(pcbData, options)
        };
      }
      
      private static generateLayerGerber(
        layer: TscircuitLayer,
        pcbData: TscircuitPcbData,
        options: GerberOptions
      ): GerberLayer {
        const commands: GerberCommand[] = [];
        
        // Add header
        commands.push(...this.generateGerberHeader(layer, options));
        
        // Add aperture definitions
        commands.push(...this.generateApertureDefinitions(layer));
        
        // Process layer features
        const features = pcbData.getLayerFeatures(layer.name);
        
        for (const feature of features) {
          switch (feature.type) {
            case 'track':
              commands.push(...this.generateTrackCommands(feature));
              break;
            case 'pad':
              commands.push(...this.generatePadCommands(feature));
              break;
            case 'via':
              commands.push(...this.generateViaCommands(feature));
              break;
            case 'fill':
              commands.push(...this.generateFillCommands(feature));
              break;
          }
        }
        
        // Add footer
        commands.push({ type: 'end_of_file', command: 'M02*' });
        
        return {
          name: layer.name,
          type: LayerTypeDetector.getLayerType(layer.name),
          commands,
          preview: this.renderLayerPreview(commands),
          statistics: this.calculateLayerStatistics(commands)
        };
      }
      
      private static renderLayerPreview(commands: GerberCommand[]): ImageData {
        const canvas = document.createElement('canvas');
        canvas.width = 1000;
        canvas.height = 1000;
        const ctx = canvas.getContext('2d')!;
        
        // Set up coordinate system
        ctx.fillStyle = 'black';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Render gerber commands
        const renderer = new GerberRenderer(ctx);
        renderer.renderCommands(commands);
        
        return ctx.getImageData(0, 0, canvas.width, canvas.height);
      }
    }

Acceptance Criteria

  • Layer Management: Complete layer stack control with visibility and transparency
  • 3D Visualization: Basic 3D board view with component models
  • DRC: Functional design rule checking with violation reporting
  • Cross-Probing: Component/net cross-referencing works accurately
  • Analysis Tools: Connectivity analysis provides useful insights
  • Gerber Preview: Can preview gerber output for manufacturing

Testing Checklist

  • Layer visibility toggles work correctly
  • 3D view renders components and board
  • DRC detects common violations
  • Cross-probing highlights related features
  • Connectivity analysis finds unconnected pins
  • Gerber preview matches expected output
  • Performance remains acceptable with advanced features enabled

Output/Deliverables

  1. Complete layer stack management system
  2. 3D visualization mode with component models
  3. Basic design rule checking engine
  4. Cross-probing system for component/net relationships
  5. Electrical analysis tools (connectivity, power)
  6. Gerber export preview functionality

Dependencies

  • Phase 4 must be completed - requires selection and information systems
  • 3D requires additional libraries - @react-three/fiber, @react-three/drei

Next Phase Dependencies

Phase 6 (Performance) depends on this phase providing:

  • Performance-heavy features to optimize
  • Complete feature set for performance testing

Estimated Time: 4-5 days
Priority: Medium-High (Professional features)
Complexity: High

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions