@typecad/typecad
Version:
🤖programmatically 💥create 🛰️hardware
850 lines • 129 kB
JavaScript
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