UNPKG

@typecad/typecad

Version:

🤖programmatically 💥create 🛰️hardware

850 lines 129 kB
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _PCB_instances, _PCB_pcb, _PCB_components, _PCB_stagedComponents, _PCB_groups, _PCB_outlines, _PCB_stagedOutlines, _PCB_options, _PCB_registryData, _PCB_existingBoardElements, _PCB_loadExistingBoardElements, _PCB_resolveNet, _PCB_addComponentToBoard, _PCB__update_footprint_node, _PCB_mapLayerToSide, _PCB_transformSexprLayers, _PCB__create_footprint_node, _PCB_cleanupRegistry; import { Component } from "./component"; import { Schematic } from "./schematic"; import fs from "node:fs"; import fsexp from "fast-sexpr"; import SExpr from "s-expression.js"; import chalk from 'chalk'; import { randomUUID } from "node:crypto"; // import { exec } from 'node:child_process'; // Moved to pcb_kicad_window.ts // import readline from 'node:readline'; // Moved to pcb_kicad_window.ts import { loadRegistry, saveRegistry, REGISTRY_FILE_PATH } from './pcb_registry'; import { scanForKiCadWindow, promptUserToSave } from './pcb_kicad_window'; import { TrackBuilder } from './pcb_track_builder'; // Re-export types and classes that other modules expect to import from './pcb' export { TrackBuilder } from './pcb_track_builder'; const S = new SExpr(); /** * Represents a printed circuit board (PCB). */ export class PCB { /** * Initializes a new PCB. * @param Boardname - Name and filename of generated files. * @param schematic - Optional schematic associated with the PCB. */ constructor(Boardname, options) { _PCB_instances.add(this); _PCB_pcb.set(this, ''); _PCB_components.set(this, []); _PCB_stagedComponents.set(this, []); // Components placed/grouped but not yet created _PCB_groups.set(this, []); _PCB_outlines.set(this, []); _PCB_stagedOutlines.set(this, []); // Outlines/tracks created but not yet included in create _PCB_options.set(this, void 0); _PCB_registryData.set(this, void 0); _PCB_existingBoardElements.set(this, []); this.Boardname = Boardname; __classPrivateFieldSet(this, _PCB_options, options || { safe_write: true, remove_orphans: true, thickness: 1.6, copper_thickness: 35, Schematic: new Schematic(Boardname) }, "f"); this.thickness = __classPrivateFieldGet(this, _PCB_options, "f").thickness || 1.6; this.copper_thickness = __classPrivateFieldGet(this, _PCB_options, "f").copper_thickness || 35; this.Schematic = __classPrivateFieldGet(this, _PCB_options, "f").Schematic || new Schematic(Boardname); __classPrivateFieldSet(this, _PCB_registryData, loadRegistry(), "f"); // REGISTRY_FILE_PATH is default in loadRegistry // Load existing board elements early so they're available for track preservation __classPrivateFieldGet(this, _PCB_instances, "m", _PCB_loadExistingBoardElements).call(this); } /** * Getter for PCB options. */ get option() { return __classPrivateFieldGet(this, _PCB_options, "f"); } /** * Places components on the board. * @param components - List of components to place. */ place(...components) { components.forEach((component) => { if (component.dnp === true) { return; } const stableComponentId = `component:${component.footprint}:${component.reference}:${component.value}:${component.mpn}:${component.pcb?.side || 'front'}`; // Register the component in the registry but don't add to board yet if (__classPrivateFieldGet(this, _PCB_registryData, "f").components && __classPrivateFieldGet(this, _PCB_registryData, "f").components[stableComponentId]) { component.uuid = __classPrivateFieldGet(this, _PCB_registryData, "f").components[stableComponentId]; // Check if component is already staged const existingStaged = __classPrivateFieldGet(this, _PCB_stagedComponents, "f").find(c => c.uuid === component.uuid); if (existingStaged) { // Update existing staged component existingStaged.pcb = component.pcb; existingStaged.value = component.value; existingStaged.reference = component.reference; existingStaged.mpn = component.mpn; existingStaged.description = component.description; existingStaged.datasheet = component.datasheet; return; } } else { if (!__classPrivateFieldGet(this, _PCB_registryData, "f").components) { __classPrivateFieldGet(this, _PCB_registryData, "f").components = {}; } __classPrivateFieldGet(this, _PCB_registryData, "f").components[stableComponentId] = component.uuid; } // Stage the component instead of immediately adding to board __classPrivateFieldGet(this, _PCB_stagedComponents, "f").push(component); }); saveRegistry(REGISTRY_FILE_PATH, __classPrivateFieldGet(this, _PCB_registryData, "f")); } /** * Groups components and/or elements from a TrackBuilder together on the board. * @param group_name - Name of the group. * @param items - A list of Component instances or TrackBuilder instances. */ group(group_name, ...items) { let uuid_list = ''; const componentsToPlaceAndProcess = []; items.forEach(item => { if (item instanceof Component) { componentsToPlaceAndProcess.push(item); } else if (item instanceof TrackBuilder) { const builderElements = item.getElements(); builderElements.forEach(element => { // UUIDs from TrackBuilder elements (tracks and vias) are added directly. // Vias are already 'placed' by the builder. Tracks aren't 'placed' components. uuid_list += `"${element.uuid}" `; }); } }); // Place all explicit components and process them if (componentsToPlaceAndProcess.length > 0) { this.place(...componentsToPlaceAndProcess); // Ensure components are registered, DNP handled etc. componentsToPlaceAndProcess.forEach((_component) => { if (_component.dnp === true) { return; } uuid_list += `"${_component.uuid}" `; // Track group membership in the component if (!_component.groups.includes(group_name)) { _component.groups.push(group_name); } }); } const existingGroupIndex = __classPrivateFieldGet(this, _PCB_groups, "f").findIndex(group => group.startsWith(`(group "${group_name}"`)); const newGroupString = `(group "${group_name}" (members ${uuid_list}))`; if (existingGroupIndex !== -1) { __classPrivateFieldGet(this, _PCB_groups, "f")[existingGroupIndex] = newGroupString; process.stdout.write(chalk.yellow.bold(`${group_name}`) + ` group updated` + '\n'); } else { __classPrivateFieldGet(this, _PCB_groups, "f").push(newGroupString); process.stdout.write(chalk.yellow.bold(`${group_name}`) + ` group added` + '\n'); } } /** * Creates and saves the board to a file. * @param items - Components and TrackBuilder instances to add to the board before creating. */ async create(...items) { // Separate components from track builders const components = []; const trackBuilders = []; items.forEach(item => { if (item instanceof Component) { components.push(item); } else if (item instanceof TrackBuilder) { trackBuilders.push(item); } }); // Only pass components to schematic (TrackBuilders don't create schematic elements) this.Schematic.create(...components); // Clear the board components and only add components explicitly passed to create() __classPrivateFieldSet(this, _PCB_components, [], "f"); // Preserve staged outlines that are not tracks (e.g., board outlines, conceptual graphics) // Only clear track-related outlines to prevent tracks from previous sessions appearing const preservedOutlines = __classPrivateFieldGet(this, _PCB_stagedOutlines, "f").filter(outline => { // Preserve outlines that are on graphical layers (not electrical tracks) return outline.elements.some(element => (element.type === 'line' && !element.layer.includes('.Cu')) || element.type === 'arc'); }); __classPrivateFieldSet(this, _PCB_outlines, [], "f"); __classPrivateFieldSet(this, _PCB_stagedOutlines, [...preservedOutlines], "f"); // Preserve non-track outlines // Note: electrical tracks need to be recreated if they should appear in the current session // Create a set of components that should be included in the board const componentsToInclude = new Set(); // Track by UUID // Add all explicitly passed components to the board components.forEach((component) => { __classPrivateFieldGet(this, _PCB_instances, "m", _PCB_addComponentToBoard).call(this, component); if (component.uuid) { componentsToInclude.add(component.uuid); } }); // Process TrackBuilder instances to include their tracks/vias in the current session trackBuilders.forEach(trackBuilder => { const builderElements = trackBuilder.getElements(); builderElements.forEach(element => { if (element.type === 'track') { // Re-create the track outline for this session const trackDetails = element.details; const trackOutline = { uuid: element.uuid, x: trackDetails.start.x, y: trackDetails.start.y, width: Math.abs(trackDetails.end.x - trackDetails.start.x), height: Math.abs(trackDetails.end.y - trackDetails.start.y), filletRadius: 0, elements: [{ type: 'line', uuid: element.uuid, layer: trackDetails.layer, strokeWidth: trackDetails.width, start: trackDetails.start, end: trackDetails.end, locked: trackDetails.locked || false }] }; __classPrivateFieldGet(this, _PCB_stagedOutlines, "f").push(trackOutline); } else if (element.type === 'via') { // Via components should already be handled by the TrackBuilder.via() method // which calls pcb.place(), so they should already be in the component system // We don't need to do anything special here for vias } }); }); // Also add any staged components that match the explicitly passed components // This handles cases where components were placed/grouped and then included in create() __classPrivateFieldGet(this, _PCB_stagedComponents, "f").forEach((stagedComponent) => { // Check if this staged component matches any explicitly passed component const matchingComponent = components.find(c => c.uuid === stagedComponent.uuid || (c.reference === stagedComponent.reference && c.footprint === stagedComponent.footprint)); if (matchingComponent && stagedComponent.uuid && !componentsToInclude.has(stagedComponent.uuid)) { // Use the staged component's position/properties if they exist if (stagedComponent.pcb) { matchingComponent.pcb = { ...stagedComponent.pcb, ...matchingComponent.pcb }; } componentsToInclude.add(stagedComponent.uuid); } }); // Include any outlines/tracks that were created during this create() session // This allows tracks created during component placement or grouping to appear __classPrivateFieldGet(this, _PCB_outlines, "f").push(...__classPrivateFieldGet(this, _PCB_stagedOutlines, "f")); const version = '(version 20241229)'; const generator = '(generator "typecad")'; const generator_version = '(generator_version "1.0")'; const general = '(general (thickness 1.6) (legacy_teardrops no))'; const paper = '(paper "A4")'; const componentMap = new Map(); __classPrivateFieldGet(this, _PCB_components, "f").forEach(comp => { if (!comp.dnp && comp.uuid && comp.via === false) { componentMap.set(comp.uuid, comp); } }); const viaMap = new Map(); __classPrivateFieldGet(this, _PCB_components, "f").forEach(comp => { if (comp.via === true && comp.uuid && comp.viaData) { const viaDataFromComponent = { ...comp.viaData }; if (comp.pcb && typeof comp.pcb.x === 'number' && typeof comp.pcb.y === 'number') { viaDataFromComponent.at = { x: comp.pcb.x, y: comp.pcb.y }; } viaMap.set(comp.uuid, viaDataFromComponent); } }); const managedOutlineElements = new Map(); __classPrivateFieldGet(this, _PCB_outlines, "f").forEach(conceptualOutline => { conceptualOutline.elements.forEach(element => { managedOutlineElements.set(element.uuid, element); }); }); // Track deleted components and vias for reporting let deletedComponents = []; let deletedVias = []; let deletedTracks = []; let existing_board_contents = []; const boardFilePath = `./build/${this.Boardname}.kicad_pcb`; const boardNetNameToCodeMap = new Map(); // Use already-loaded existing board elements from constructor if (__classPrivateFieldGet(this, _PCB_existingBoardElements, "f").length > 0) { // Reconstruct existing_board_contents from #existingBoardElements existing_board_contents = ['kicad_pcb', ...__classPrivateFieldGet(this, _PCB_existingBoardElements, "f")]; } let final_board_contents = []; let headerItems = [version, generator, generator_version, general, paper]; if (existing_board_contents.length > 1) { existing_board_contents.slice(1).forEach(item => { if (typeof item === 'string' && item.startsWith('(')) { final_board_contents.push(item); } else if (Array.isArray(item) && item.length > 0) { const nodeType = item[0]; if (nodeType === 'net' && item.length >= 3 && !isNaN(Number(item[1])) && (typeof item[2] === 'string' || typeof item[2] === 'symbol')) { const netCode = Number(item[1]); let originalNetNameFromFile = String(item[2]); let netNameForMapKey = originalNetNameFromFile; if (netNameForMapKey.startsWith('`') && netNameForMapKey.endsWith('`')) { netNameForMapKey = netNameForMapKey.substring(1, netNameForMapKey.length - 1); } else if (netNameForMapKey.startsWith('"') && netNameForMapKey.endsWith('"')) { netNameForMapKey = netNameForMapKey.substring(1, netNameForMapKey.length - 1); } if (netNameForMapKey.startsWith('/')) { netNameForMapKey = netNameForMapKey.substring(1); } netNameForMapKey = netNameForMapKey.toLowerCase(); if (netNameForMapKey) { boardNetNameToCodeMap.set(netNameForMapKey, netCode); } final_board_contents.push(item); } else if (nodeType !== 'footprint' && nodeType !== 'group' && nodeType !== 'gr_rect' && nodeType !== 'gr_line' && nodeType !== 'gr_arc' && nodeType !== 'via' && nodeType !== 'segment') { final_board_contents.push(item); } } }); headerItems.forEach(header => { const headerSymbol = header.substring(1, header.indexOf(" ")); if (!final_board_contents.some(item => (typeof item === 'string' && item.includes(`(${headerSymbol}`)) || (Array.isArray(item) && item[0] === headerSymbol))) { final_board_contents.unshift(header); } }); } else { final_board_contents = [...headerItems]; } const schematicNetDefinitions = []; if (!this.Schematic) { console.warn(chalk.yellow(`[PCB CREATE] Warning: No schematic provided. Net information will be limited to existing board nets.`)); } else if (this.Schematic.Nodes) { this.Schematic.Nodes.forEach(schematicNet => { schematicNetDefinitions.push(['net', schematicNet.code, `\`${schematicNet.name}\``]); }); } let boardContentsWithoutNets = final_board_contents.filter(item => { // Keep everything except net definitions - we'll handle nets separately if (Array.isArray(item) && item[0] === 'net') { return false; } if (typeof item === 'string' && item.trim().startsWith('(net ')) { try { const parsedItem = fsexp(item).pop(); if (Array.isArray(parsedItem) && parsedItem[0] === 'net') return false; } catch (e) { /* ignore */ } } return true; }); // Collect existing nets from the board file to preserve them const existingNets = new Map(); // net name -> net definition const existingNetCodes = new Set(); final_board_contents.forEach(item => { if (Array.isArray(item) && item[0] === 'net') { const netCode = parseInt(String(item[1])); const netName = String(item[2] || '').replace(/["`]/g, ''); if (!isNaN(netCode)) { // Use netCode as the primary key to prevent duplicate net codes const netKey = `${netCode}:${netName}`; existingNets.set(netKey, item); existingNetCodes.add(netCode); } } else if (typeof item === 'string' && item.trim().startsWith('(net ')) { try { const parsedItem = fsexp(item).pop(); if (Array.isArray(parsedItem) && parsedItem[0] === 'net') { const netCode = parseInt(String(parsedItem[1])); const netName = String(parsedItem[2] || '').replace(/["`]/g, ''); if (!isNaN(netCode)) { // Use netCode as the primary key to prevent duplicate net codes const netKey = `${netCode}:${netName}`; existingNets.set(netKey, parsedItem); existingNetCodes.add(netCode); } } } catch (e) { /* ignore */ } } }); // Merge schematic nets with existing nets, ensuring unique net names and codes. const allNets = []; const mergedNetCodes = new Set(); // Tracks codes used in the final 'allNets' list. const finalNetDefinitionsByName = new Map(); // Key: normalized net name, Value: final net S-expression array // Pass 1: Add existing board nets. Prioritize these definitions. // `existingNets` (Map<`${code}:${name}`, netDefArray>) is populated from the board file. // `boardNetNameToCodeMap` (normalizedName -> code) is also from the board file. // Create a temporary map of board nets by normalized name for easier lookup and ensuring unique names from board. const boardNetsProcessedByName = new Map(); // normalizedName -> chosen board netDef if (existingNets && existingNets.size > 0) { const sortedExistingNetDefs = Array.from(existingNets.values()).sort((a, b) => parseInt(String(a[1])) - parseInt(String(b[1]))); for (const netDef of sortedExistingNetDefs) { const name = String(netDef[2]).replace(/[`"]/g, ''); let normalizedName = name.toLowerCase(); if (normalizedName.startsWith('/')) { normalizedName = normalizedName.substring(1); } if (!boardNetsProcessedByName.has(normalizedName)) { // Keep first encountered definition for a given name boardNetsProcessedByName.set(normalizedName, netDef); } } } boardNetsProcessedByName.forEach((netDef, normalizedName) => { const code = parseInt(String(netDef[1])); finalNetDefinitionsByName.set(normalizedName, netDef); mergedNetCodes.add(code); }); // Determine max code for potential renumbering new schematic nets let maxCodeInUse = 0; mergedNetCodes.forEach(c => maxCodeInUse = Math.max(maxCodeInUse, c)); schematicNetDefinitions.forEach(schNet => maxCodeInUse = Math.max(maxCodeInUse, parseInt(String(schNet[1])))); const getNextAvailableNetIdHelper = (currentMax, usedSet) => { let id = currentMax + 1; while (usedSet.has(id)) { id++; } return id; }; // Pass 2: Integrate schematic nets. schematicNetDefinitions.forEach((schematicNetDef) => { const originalSchCode = parseInt(String(schematicNetDef[1])); const schNameSExpr = schematicNetDef[2]; const schName = String(schNameSExpr).replace(/[`"]/g, ''); let normalizedSchName = schName.toLowerCase(); if (normalizedSchName.startsWith('/')) { normalizedSchName = normalizedSchName.substring(1); } if (finalNetDefinitionsByName.has(normalizedSchName)) { // Net name already exists (came from board). Board's definition is authoritative. // Ensure boardNetNameToCodeMap (used by #resolveNet) reflects the board's code for this name. const boardDef = finalNetDefinitionsByName.get(normalizedSchName); const boardCode = parseInt(String(boardDef[1])); boardNetNameToCodeMap.set(normalizedSchName, boardCode); } else { // This is a new net name from the schematic. let finalCode = originalSchCode; if (mergedNetCodes.has(originalSchCode)) { // Code conflict with an existing *different* named net. finalCode = getNextAvailableNetIdHelper(maxCodeInUse, mergedNetCodes); maxCodeInUse = finalCode; // Update max for next potential renumbering console.log(chalk.yellow(`[Net Create] Schematic net "${schName}" (original code ${originalSchCode}) renumbered to ${finalCode} due to code conflict.`)); } const newNetDefArray = ['net', finalCode, schNameSExpr]; finalNetDefinitionsByName.set(normalizedSchName, newNetDefArray); mergedNetCodes.add(finalCode); boardNetNameToCodeMap.set(normalizedSchName, finalCode); // Update map for #resolveNet with the final code } }); // Ensure net 0 "" (unconnected) exists. const emptyNetNormalizedKey = ""; // Normalized key for the unnamed net if (!finalNetDefinitionsByName.has(emptyNetNormalizedKey)) { if (!mergedNetCodes.has(0)) { finalNetDefinitionsByName.set(emptyNetNormalizedKey, ['net', 0, '``']); mergedNetCodes.add(0); // Ensure 0 is marked as used boardNetNameToCodeMap.set(emptyNetNormalizedKey, 0); } else { // Code 0 is taken by a named net. This is unusual. // KiCad expects (net 0 "") to exist. We might need to shift the other net. // For now, log a warning. A more robust solution might re-evaluate all nets if 0 is taken by a named net. console.warn(chalk.red(`[Net Create] Cannot add (net 0 "") because code 0 is already in use by a named net. PCB might be invalid.`)); } } else { // Net "" exists, ensure its code is 0. const def = finalNetDefinitionsByName.get(emptyNetNormalizedKey); const currentCodeForEmptyNet = parseInt(String(def[1])); if (currentCodeForEmptyNet !== 0) { if (!mergedNetCodes.has(0)) { // If 0 is free mergedNetCodes.delete(currentCodeForEmptyNet); // Free up its old code def[1] = 0; // Set to code 0 mergedNetCodes.add(0); boardNetNameToCodeMap.set(emptyNetNormalizedKey, 0); } else { // Code 0 is taken by another named net AND "" net has a non-zero code. console.warn(chalk.red(`[Net Create] Net "" exists with code ${currentCodeForEmptyNet}, but code 0 is taken. PCB might be invalid.`)); } } } // Populate allNets from finalNetDefinitionsByName, sorted by code. const sortedFinalNetDefs = Array.from(finalNetDefinitionsByName.values()).sort((a, b) => parseInt(String(a[1])) - parseInt(String(b[1]))); sortedFinalNetDefs.forEach(def => { allNets.push(def); }); let insertionIndex = -1; const preferredOrderMarkers = ['setup', 'layers', 'paper', 'general', 'generator_version', 'generator', 'version']; for (const marker of preferredOrderMarkers) { for (let i = 0; i < boardContentsWithoutNets.length; i++) { const currentItem = boardContentsWithoutNets[i]; let itemType = ''; if (Array.isArray(currentItem) && typeof currentItem[0] === 'string') { itemType = currentItem[0]; } else if (typeof currentItem === 'string' && currentItem.startsWith('(')) { try { const parsed = fsexp(currentItem).pop(); if (Array.isArray(parsed) && typeof parsed[0] === 'string') { itemType = parsed[0]; } } catch (e) { /* ignore */ } } if (itemType === marker) { insertionIndex = i + 1; } } if (insertionIndex !== -1 && marker === 'setup') break; } if (insertionIndex === -1) { let count = 0; const initialHeaders = ['version', 'generator', 'generator_version', 'general', 'paper', 'layers', 'setup']; for (let i = 0; i < boardContentsWithoutNets.length; ++i) { const currentItem = boardContentsWithoutNets[i]; let itemType = ''; if (Array.isArray(currentItem) && typeof currentItem[0] === 'string') { itemType = currentItem[0]; } else if (typeof currentItem === 'string' && currentItem.startsWith('(')) { try { const parsed = fsexp(currentItem).pop(); if (Array.isArray(parsed) && typeof parsed[0] === 'string') { itemType = parsed[0]; } } catch (e) { /* ignore */ } } if (initialHeaders.includes(itemType)) count = i + 1; else break; } insertionIndex = count > 0 ? count : (boardContentsWithoutNets.findIndex(item => typeof item !== 'string' || !item.startsWith('('))); if (insertionIndex === -1) insertionIndex = boardContentsWithoutNets.length; } if (allNets.length > 0) { boardContentsWithoutNets.splice(insertionIndex, 0, ...allNets); } final_board_contents = boardContentsWithoutNets; __classPrivateFieldGet(this, _PCB_groups, "f").forEach((groupString) => { try { const groupNode = fsexp(groupString.replaceAll('"', "`")).pop(); if (groupNode) { final_board_contents.push(groupNode); } } catch (e) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; console.error(chalk.red(`👺 Error: Failed to parse group string: ${groupString} - ${e.message}${callSiteInfo}`)); } }); __classPrivateFieldSet(this, _PCB_groups, [], "f"); if (existing_board_contents.length > 1) { existing_board_contents.slice(1).forEach(item => { if (Array.isArray(item) && item[0] === 'footprint') { let uuid = null; let existingAtNode = null; for (const subItem of item) { if (Array.isArray(subItem)) { if (subItem[0] === 'uuid' && typeof subItem[1] === 'string') { uuid = subItem[1].replaceAll('`', ''); } else if (subItem[0] === 'at' && subItem.length >= 3) { existingAtNode = subItem; } } if (uuid && existingAtNode) break; } if (uuid && !existingAtNode) { for (const subItem of item) { if (Array.isArray(subItem) && subItem[0] === 'at' && subItem.length >= 3) { existingAtNode = subItem; break; } } } if (uuid && componentMap.has(uuid)) { const component = componentMap.get(uuid); if (existingAtNode) { const existingX = parseFloat(String(existingAtNode[1])); const existingY = parseFloat(String(existingAtNode[2])); let existingRotation = 0; if (existingAtNode.length > 3 && typeof existingAtNode[3] !== 'undefined') { const parsedRot = parseFloat(String(existingAtNode[3])); if (!isNaN(parsedRot)) { existingRotation = parsedRot; } } if (!isNaN(existingX) && !isNaN(existingY)) { component.pcb.x = existingX; component.pcb.y = existingY; component.pcb.rotation = existingRotation; } else { component.pcb.rotation = component.pcb.rotation ?? 0; } } else { component.pcb.rotation = component.pcb.rotation ?? 0; } try { for (const subItem of item) { if (Array.isArray(subItem) && subItem[0] === 'pad') { for (const padSubItem of subItem) { if (Array.isArray(padSubItem) && padSubItem[0] === 'at') { if (padSubItem.length === 3) { padSubItem.push(component.pcb.rotation); } else if (padSubItem.length === 4) { padSubItem[3] = component.pcb.rotation - parseFloat(String(padSubItem[3])); } } } } } const updatedNode = __classPrivateFieldGet(this, _PCB_instances, "m", _PCB__update_footprint_node).call(this, item, component, boardNetNameToCodeMap); final_board_contents.push(updatedNode); componentMap.delete(uuid); } catch (e) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; console.error(chalk.red(`👺 Error: Failed to update footprint node for ${component.reference || uuid}: ${e.message}${callSiteInfo}`)); } } else { // Component exists in board file but not in current component list - conditionally delete or keep based on options if (__classPrivateFieldGet(this, _PCB_options, "f").remove_orphans) { // Delete orphaned component when remove_orphans is true if (uuid) { // Try to extract component reference for better reporting let componentReference = uuid; // fallback to UUID for (const subItem of item) { if (Array.isArray(subItem) && subItem[0] === 'property' && subItem.length > 2 && String(subItem[1]) === '`Reference`') { // Extract reference value, handling both single string and multiple elements let refValue = ''; for (let k = 2; k < subItem.length; k++) { if (typeof subItem[k] === 'string' && !Array.isArray(subItem[k])) { refValue += subItem[k]; } else { break; // Stop at first non-string (likely a sub-node) } } if (refValue) { componentReference = refValue.replaceAll('`', ''); break; } } } deletedComponents.push(componentReference); } // Do not add to final_board_contents (effectively deletes it from the board) } else { // Keep orphaned component when remove_orphans is false final_board_contents.push(item); } } } else if (Array.isArray(item) && item[0] === 'via') { let viaUuid = null; let viaAtNode = null; for (const subItem of item) { if (Array.isArray(subItem)) { if (subItem[0] === 'uuid' && typeof subItem[1] === 'string') { viaUuid = subItem[1].replaceAll('`', ''); } else if (subItem[0] === 'at' && subItem.length >= 3) { viaAtNode = subItem; } } if (viaUuid && viaAtNode) break; } if (viaUuid && !viaAtNode) { for (const subItem of item) { if (Array.isArray(subItem) && subItem[0] === 'at' && subItem.length >= 3) { viaAtNode = subItem; break; } } } if (viaUuid && viaMap.has(viaUuid)) { const viaDataFromMap = viaMap.get(viaUuid); const viaComponent = __classPrivateFieldGet(this, _PCB_components, "f").find(c => c.uuid === viaUuid && c.via === true); if (viaAtNode) { const existingX = parseFloat(String(viaAtNode[1])); const existingY = parseFloat(String(viaAtNode[2])); if (!isNaN(existingX) && !isNaN(existingY)) { viaDataFromMap.at = { x: existingX, y: existingY }; if (viaComponent) { if (viaComponent.pcb) { viaComponent.pcb.x = existingX; viaComponent.pcb.y = existingY; } else { viaComponent.pcb = { x: existingX, y: existingY, rotation: 0 }; } if (viaComponent.viaData) { viaComponent.viaData.at = { x: existingX, y: existingY }; } } } } } else { // Via exists in board file but not in current via list - conditionally delete or keep based on options // Check if this via was originally created by TypeCAD (exists in registry) const wasCreatedByTypeCAD = viaUuid && __classPrivateFieldGet(this, _PCB_registryData, "f").vias && Object.values(__classPrivateFieldGet(this, _PCB_registryData, "f").vias).includes(viaUuid); if (wasCreatedByTypeCAD && __classPrivateFieldGet(this, _PCB_options, "f").remove_orphans) { // This is an orphaned TypeCAD-generated via - remove it if (viaUuid) { let viaLocationDescription = viaUuid; // fallback to UUID if (viaAtNode && viaAtNode.length >= 3) { const x = parseFloat(String(viaAtNode[1])); const y = parseFloat(String(viaAtNode[2])); if (!isNaN(x) && !isNaN(y)) { viaLocationDescription = `TypeCAD via at (${x}, ${y})`; } } deletedVias.push(viaLocationDescription); } // Do not add to final_board_contents (effectively deletes it from the board) } else { // Either this is a manually-created via in KiCad, or remove_orphans is false - keep it final_board_contents.push(item); if (!wasCreatedByTypeCAD) { // This is a manually created via in KiCad - preserve it let viaDescription = "manually created via"; if (viaAtNode && viaAtNode.length >= 3) { const x = parseFloat(String(viaAtNode[1])); const y = parseFloat(String(viaAtNode[2])); if (!isNaN(x) && !isNaN(y)) { viaDescription = `manually created via at (${x.toFixed(2)}, ${y.toFixed(2)})`; } } const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; console.log(chalk.cyan(`[PCB Preservation] Preserving ${viaDescription}${callSiteInfo}`)); } } } } else if (Array.isArray(item) && (item[0] === 'gr_line' || item[0] === 'segment' || item[0] === 'gr_arc' || item[0] === 'gr_rect')) { let fileElementUuid = null; let startNode = null; let endNode = null; let midNode = null; let locked = null; for (const subItem of item) { if (Array.isArray(subItem)) { if (subItem[0] === 'uuid' && typeof subItem[1] === 'string') { fileElementUuid = subItem[1].replaceAll('`', ''); } else if (subItem[0] === 'start' && subItem.length >= 3) { startNode = subItem; } else if (subItem[0] === 'end' && subItem.length >= 3) { endNode = subItem; } else if (item[0] === 'gr_arc' && subItem[0] === 'mid' && subItem.length >= 3) { midNode = subItem; } else if (item[0] === 'segment' && subItem[0] === 'locked') { locked = String(subItem[1]).toLowerCase() === 'yes'; } } } if (fileElementUuid && managedOutlineElements.has(fileElementUuid)) { const managedElement = managedOutlineElements.get(fileElementUuid); if ((managedElement.type === 'line') && startNode && endNode) { const sx = parseFloat(String(startNode[1])); const sy = parseFloat(String(startNode[2])); const ex = parseFloat(String(endNode[1])); const ey = parseFloat(String(endNode[2])); if (!isNaN(sx) && !isNaN(sy) && !isNaN(ex) && !isNaN(ey)) { managedElement.start = { x: sx, y: sy }; managedElement.end = { x: ex, y: ey }; if (item[0] === 'segment' && locked !== null) { managedElement.locked = locked; } if (managedElement.type === 'line' && item[0] === 'gr_arc' && midNode) { } else if (item[0] === 'gr_arc' && midNode) { const arcElement = managedOutlineElements.get(fileElementUuid); if (arcElement.type === 'arc') { const mx = parseFloat(String(midNode[1])); const my = parseFloat(String(midNode[2])); if (!isNaN(mx) && !isNaN(my)) { arcElement.mid = { x: mx, y: my }; } } } } } managedOutlineElements.delete(fileElementUuid); } else { // Check if this element was originally created by TypeCAD (exists in registry) const wasCreatedByTypeCAD = fileElementUuid && __classPrivateFieldGet(this, _PCB_registryData, "f").outlineElements && Object.values(__classPrivateFieldGet(this, _PCB_registryData, "f").outlineElements).includes(fileElementUuid); if (wasCreatedByTypeCAD && __classPrivateFieldGet(this, _PCB_options, "f").remove_orphans) { // This is an orphaned TypeCAD-generated element - remove it if (fileElementUuid) { let trackDescription = fileElementUuid; // fallback to UUID const elementType = item[0]; // Create a more descriptive name for the deleted track if (startNode && endNode && startNode.length >= 3 && endNode.length >= 3) { const sx = parseFloat(String(startNode[1])); const sy = parseFloat(String(startNode[2])); const ex = parseFloat(String(endNode[1])); const ey = parseFloat(String(endNode[2])); if (!isNaN(sx) && !isNaN(sy) && !isNaN(ex) && !isNaN(ey)) { trackDescription = `${elementType} from (${sx.toFixed(2)}, ${sy.toFixed(2)}) to (${ex.toFixed(2)}, ${ey.toFixed(2)})`; } } else if (elementType === 'gr_rect') { trackDescription = `${elementType} (UUID: ${fileElementUuid.substring(0, 8)}...)`; } deletedTracks.push(trackDescription); } // Do not add to final_board_contents (effectively deletes it from the board) } else { // Either this is a manually-added element, or remove_orphans is false - keep it final_board_contents.push(item); } } } }); } // Report deletions if any occurred if (__classPrivateFieldGet(this, _PCB_options, "f").remove_orphans) { if (deletedComponents.length > 0) { console.log(chalk.red(`[PCB Cleanup] Removed ${deletedComponents.length} orphaned component(s) from board: ${deletedComponents.join(', ')}`)); } if (deletedVias.length > 0) { console.log(chalk.red(`[PCB Cleanup] Removed ${deletedVias.length} orphaned TypeCAD-generated via(s) from board: ${deletedVias.join(', ')}`)); } if (deletedTracks.length > 0) { console.log(chalk.red(`[PCB Cleanup] Removed ${deletedTracks.length} orphaned TypeCAD-generated track/outline element(s) from board: ${deletedTracks.join(', ')}`)); } } componentMap.forEach(component => { try { const newNode = __classPrivateFieldGet(this, _PCB_instances, "m", _PCB__create_footprint_node).call(this, component, boardNetNameToCodeMap); fin