@typecad/typecad
Version:
π€programmatically π₯create π°οΈhardware
529 lines (527 loc) β’ 21.3 kB
JavaScript
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 _Schematic_options, _Schematic_groupedComponents;
import { Pin } from "./pin";
import fs from "node:fs";
import Handlebars from "handlebars";
import chalk from 'chalk';
import { erc } from "./erc";
import { randomUUID } from "node:crypto";
let schematic_template = `(export (version "E")
(design
(tool "typeCAD 0.0.30"))
(components
{{#each components}}
{{this}}
{{/each}})
(nets
{{#each nets}}
{{this}}
{{/each}}
))
`;
let comp_template = `(comp
(ref \"{{reference}}\")
(value \"{{value}}\")
(footprint \"{{footprint}}\")
(fields
{{#if footprint}}
(field (name \"Footprint\") \"{{footprint}}\")
{{/if}}
{{#if datasheet}}
(field (name \"Datasheet\")\"{{datasheet}}\")
{{/if}}
{{#if description}}
(field (name \"Description\")\"{{description}}\")
{{/if}}
{{#if voltage}}
(field (name \"Voltage\")"{{voltage}}")
{{/if}}
{{#if wattage}}
(field (name \"Wattage\")"{{wattage}}")
{{/if}}
{{#if mpn}}
(field (name \"MPN\")"{{mpn}}")
{{/if}}
)
)`;
let nets_template = `{{#each nets}}
(net (code "{{code}}") (name "{{name}}")
{{#each nodes}}
(node (ref "{{this.reference}}") (pin "{{this.number}}") (pintype "{{this.type}}"))
{{/each}}
)
{{/each}}`;
/**
* Recursively freezes an object to make it immutable.
*
* @template T
* @param {T} obj - The object to freeze.
* @returns {Readonly<T>} - The frozen object.
* @ignore
*/
function deepFreeze(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj; // Not an object, no need to freeze
}
Object.keys(obj).forEach(key => {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
deepFreeze(value); // Recursively freeze nested objects
}
});
return Object.freeze(obj);
}
/**
* The main class for typeCAD. Holds all {@link Component} classes, creates work files, and creates nets.
*
* @export
* @class Schematic
* @typedef {Schematic}
*/
export class Schematic {
/**
* Getter for Schematic options.
*
* @example
* ```ts
* let schematic = new Schematic('sheetname');
* schematic.option.safe_write = false;
* schematic.option.build_dir = './custom_build/';
* ```
*/
get option() {
return __classPrivateFieldGet(this, _Schematic_options, "f");
}
/**
* Used internally
* @ignore
*/
_storeNetParams(name, code, ...nodes) {
this.Nodes.push({ name, code, nodes, owner: null });
}
bom(output_folder) {
let bom = '';
let _output_folder = output_folder || './build';
// Create header from configured fields
bom += __classPrivateFieldGet(this, _Schematic_options, "f").bom_fields.join(__classPrivateFieldGet(this, _Schematic_options, "f").bom_separator) + '\n';
// Filter out VIA components (pattern: VIA followed by numbers, case insensitive)
const viaPattern = /^VIA\d+$/i;
this.Components.forEach(component => {
// Skip components with reference designators matching VIA pattern
if (component.reference && viaPattern.test(component.reference)) {
return; // Skip this component
}
const fields = __classPrivateFieldGet(this, _Schematic_options, "f").bom_fields.map(field => {
switch (field.toLowerCase()) {
case 'reference': return component.reference;
case 'value': return component.value;
case 'datasheet': return component.datasheet;
case 'footprint': return component.footprint;
case 'mpn': return component.mpn;
case 'description': return component.description;
case 'voltage': return component.voltage;
case 'wattage': return component.wattage;
default: return '';
}
});
bom += fields.join(__classPrivateFieldGet(this, _Schematic_options, "f").bom_separator) + '\n';
});
try {
fs.writeFileSync(`${_output_folder}/${this.Sheetname}.csv`, bom);
process.stdout.write(chalk.cyan.bold(`${_output_folder}/${this.Sheetname}.csv`) + ` BOM written` + '\n');
}
catch (err) {
console.error(err);
return false;
}
}
/**
* Initializes a new schematic with a given sheet name.
*
* @param {string} Sheetname - Name and filename of generated files.
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* ```
*/
constructor(Sheetname) {
this.Components = [];
this.Sheetname = '';
this.uuid = randomUUID();
this.sxexp_components = [];
this.sxexp_nets = [];
this.code_counter = 0;
this.Nodes = [];
this.merged_nets = [];
this._chained_name = '';
_Schematic_options.set(this, {
net_prefix: 'net',
bom_fields: ['Reference', 'Value', 'Datasheet', 'Footprint', 'MPN'],
bom_separator: ','
});
_Schematic_groupedComponents.set(this, new Map());
this.Sheetname = Sheetname;
}
/**
* Adds components to the schematic.
*
* @param {...Component[]} components - Components to add to the schematic.
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* let r1 = new Component({});
* let r2 = new Component({});
* typecad.add(r1, r2);
* ```
*/
add(...components) {
components.forEach(component => {
if (component.dnp === true) {
return;
}
this.Components.push(component);
let template = Handlebars.compile(comp_template);
let comp_data = { reference: component.reference, value: component.value, footprint: component.footprint };
if (component.wattage) {
comp_data.wattage = component.wattage;
}
if (component.voltage) {
comp_data.voltage = component.voltage;
}
if (component.datasheet) {
comp_data.datasheet = component.datasheet;
}
if (component.description) {
comp_data.description = component.description;
}
if (component.mpn) {
comp_data.mpn = component.mpn;
}
let _comp = template(comp_data);
this.sxexp_components.push(_comp);
// Track components by group
const groups = component.getGroups();
if (groups.length > 0) {
groups.forEach(group => {
if (!__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").has(group)) {
__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").set(group, []);
}
__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").get(group)?.push(component);
});
}
else {
// Add to default group if no groups specified
if (!__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").has('Project')) {
__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").set('Project', []);
}
__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").get('Project')?.push(component);
}
if (component.via === true) {
let netName;
const viaComponentUUID = component.uuid;
for (const net of this.Nodes) {
if (net.nodes.some(pinInNet => pinInNet.uuid === viaComponentUUID && pinInNet.number === "1")) {
netName = net.name;
break;
}
}
const displayNetName = netName ? chalk.yellowBright(netName) : chalk.gray('unnamed');
const groupInfo = component.getGroups().length > 0
? chalk.yellow(`[${component.getGroups().join(', ')}] `)
: '';
// process.stdout.write(chalk.cyanBright('Via') + ` added to net '${displayNetName}' ` + groupInfo + '\n');
}
else {
const displayReference = component.reference && component.reference.trim()
? chalk.blue.bold(component.reference) + ': '
: chalk.gray('Component: ');
const displayValue = component.value && component.value.trim()
? chalk.cyan(`(${component.value}) `)
: '';
const displayDescription = component.description && component.description.trim()
? chalk.italic(component.description) + ' '
: '';
const groupInfo = component.getGroups().length > 0
? chalk.yellow(`[${component.getGroups().join(', ')}] `)
: '';
// process.stdout.write(displayReference + displayValue + displayDescription + 'added ' + groupInfo + '\n');
}
// deepFreeze(component);
});
}
/**
* Adds a no-connection flag to a pin.
*
* @param {...Pin[]} pins - Pins to mark as no-connect.
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* let r1 = new Resistor({ symbol: "Device:R_Small", reference: 'R1' });
* typecad.dnc(r1.pin(1));
* ```
*/
dnc(...pins) {
pins.forEach((pin) => {
pin.type = 'no_connect';
this.net(pin);
});
}
/**
* Sets a name for a net.
*
* @param {{pins: Pin[]}} pins: Pin[]}
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* let r1 = new Component({});
* let r2 = new Component({});
*
* // named net
* typecad.named('vin').net(r1.pin(1), r2.pin(1));
*
* // unnamed net
* typecad.net(r1.pin(1), r2.pin(1));
* ```
*/
named(name) {
this._chained_name = name;
return this;
}
/**
* Connects a group of pins together.
*
* @param {...Pin[]} pins - Pins to connect.
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* let r1 = new Component({});
* let r2 = new Component({});
*
* // named net
* typecad.named('vin').net(r1.pin(1), r2.pin(1));
*
* // unnamed net
* typecad.net(r1.pin(1), r2.pin(1));
* ```
*/
net(...pins) {
this.code_counter++;
// make a net name using configured prefix
let node_name = this._chained_name ? this._chained_name : `${__classPrivateFieldGet(this, _Schematic_options, "f").net_prefix}${this.code_counter}`;
// check each pin in each node to see if a pin is connected somewhere else
// if so, merge the nets by making their names the same
pins.forEach((pin) => {
if (!(pin instanceof Pin)) {
this.error(`Invalid object passed to net(). Expected Pin, received ${typeof pin}`);
}
this.Nodes.forEach((netParam, index) => {
netParam.nodes.forEach((netParamPin) => {
if (netParamPin.reference === pin.reference && netParamPin.number === pin.number) {
// Determine which name to keep - prefer named nets over auto-generated ones
const currentIsNamed = !node_name.startsWith('net');
const existingIsNamed = !netParam.name.startsWith('net');
let finalNetName;
let mergedNetInfo;
if (currentIsNamed && !existingIsNamed) {
// Current net has a name, existing is auto-generated - keep current name
finalNetName = node_name;
mergedNetInfo = { old_name: netParam.name, merged_to_number: this.code_counter };
// Update the existing net's name to the meaningful one
this.Nodes[index].name = finalNetName;
if (node_name !== netParam.name) {
process.stdout.write(chalk.gray.bold(`${netParam.name}`) + ` net merged into ` + chalk.gray.bold(`${node_name}`) + '\n');
}
}
else if (!currentIsNamed && existingIsNamed) {
// Current net is auto-generated, existing has a name - keep existing name
finalNetName = netParam.name;
mergedNetInfo = { old_name: node_name, merged_to_number: this.Nodes[index].code };
if (node_name !== netParam.name) {
process.stdout.write(chalk.gray.bold(`${node_name}`) + ` net merged into ` + chalk.gray.bold(`${netParam.name}`) + '\n');
}
}
else {
// Both are named or both are auto-generated - use existing behavior
finalNetName = netParam.name;
mergedNetInfo = { old_name: node_name, merged_to_number: this.Nodes[index].code };
if (currentIsNamed && node_name !== netParam.name) {
process.stdout.write(chalk.gray.bold(`${node_name}`) + ` net merged into ` + chalk.gray.bold(`${netParam.name}`) + '\n');
}
}
this.merged_nets.push(mergedNetInfo);
node_name = finalNetName;
}
});
});
});
// store the node
this._storeNetParams(node_name, this.code_counter, ...pins);
// Check for duplicate node names and merge them
let nodeMap = {};
this.Nodes.forEach((node) => {
if (nodeMap[node.name]) {
nodeMap[node.name].nodes.push(...node.nodes);
}
else {
nodeMap[node.name] = { ...node, nodes: [...node.nodes] };
}
});
// remove duplicate pins
Object.values(nodeMap).forEach((node) => {
node.nodes = node.nodes.filter((pin, index, self) => index === self.findIndex((p) => (p && pin && p.reference === pin.reference && p.number === pin.number)));
});
this.Nodes = Object.values(nodeMap);
// clear the name
this._chained_name = '';
// console.log(` β° net ${node_name}`) // node_name here holds the determined net name before _chained_name is cleared
// The following block seems redundant or problematic and might be causing issues
// with how nets (especially for vias) are being finalized or duplicated.
// The primary net storage and merging happens before this.
/*
let _net_display = '';
pins.forEach((pin) => {
_net_display += '~ ' + pin.reference + ':' + pin.number + ' ';
});
let exists = false;
this.Nodes.forEach((node) => {
if (node.name === this._chained_name) { // _chained_name is likely empty here
exists = true;
node.nodes.push(...pins);
}
});
if (!exists) {
this.code_counter++;
this._storeNetParams(this._chained_name, this.code_counter, ...pins);
console.log(chalk.magenta(`[SCHEMATIC NET DEBUG] Stored new named net: Name = ${this._chained_name}, Code = ${this.code_counter}`));
}
this._chained_name = '';
*/
}
/**
* Used internally
* @ignore
*/
make_sexp_net() {
// create the entire net list sexp from the netNodes array
let template = Handlebars.compile(nets_template);
let _nets = template({ nets: this.Nodes });
this.sxexp_nets.push(_nets);
}
/**
* Creates schematic files.
*
* @param {...Component[]} components - All the components to be included in the schematic.
* @example
* ```ts
* let typecad = new Schematic('sheetname');
* let r1 = new Component({});
* let r2 = new Component({});
* typecad.create(r1, r2);
* ```
*/
create(...component) {
process.stdout.write('π ' + chalk.whiteBright.bold('type') + 'CAD Project: ' + chalk.whiteBright.bold(this.Sheetname) + '\n');
component.forEach((component) => {
this.add(component);
});
this.displayGroupHierarchy();
this.make_sexp_net();
let template = Handlebars.compile(schematic_template, { noEscape: true });
let schematic_data = { components: this.sxexp_components, nets: this.sxexp_nets };
let _schematic = template(schematic_data);
try {
fs.writeFileSync(`./build/${this.Sheetname}.net`, _schematic);
process.stdout.write('\nπ ' + chalk.whiteBright.bold('type') + 'CAD finished\n');
process.stdout.write('βββ π Output\n');
process.stdout.write(' βββ ' + chalk.green(`./build/${this.Sheetname}.net`) + '\n');
}
catch (err) {
console.error(err);
process.exit(1);
return false;
}
}
/**
* Performs electrical rule checks.
*/
erc() {
erc(this);
}
/**
* Logs an error message and exits.
*
* @param {string} error - The error message to log.
*/
error(error) {
process.stdout.write(chalk.bgRed(`πΊ Error:`) + chalk.bold(` ${error}` + '\n'));
process.exit(1);
}
/**
* Logs a warning message.
*
* @param {string} warning - The warning message to log.
*/
warn(warning) {
process.stdout.write(chalk.bgYellow(`WARN:`) + chalk.bold(` ${warning}` + '\n'));
}
getComponentIcon(component) {
if (component.via)
return 'π³οΈ ';
if (component.reference?.startsWith('C'))
return 'πͺ« ';
if (component.reference?.startsWith('R'))
return 'π';
if (component.reference?.startsWith('L'))
return 'π';
if (component.reference?.startsWith('SW'))
return 'π³ ';
if (component.reference?.startsWith('LED'))
return 'π¨ ';
if (component.reference?.startsWith('D'))
return 'πΊ ';
if (component.reference?.startsWith('U'))
return 'μΉ©';
if (component.reference?.startsWith('F'))
return 'π ';
return 'βΌοΈ ';
}
formatComponentDisplay(component) {
const icon = this.getComponentIcon(component);
let display = `${icon} ${component.reference}`;
if (component.value) {
display += `: ${component.value}`;
}
if (component.description) {
display += ` (${component.description})`;
}
if (component.via) {
const netName = this.Nodes.find(net => net.nodes.some(pin => pin.uuid === component.uuid))?.name;
if (netName) {
display += ` (Net: '${netName}')`;
}
}
return display;
}
displayGroupHierarchy() {
process.stdout.write('βββ π Groups\n');
// Sort groups alphabetically
const sortedGroups = Array.from(__classPrivateFieldGet(this, _Schematic_groupedComponents, "f").keys()).sort();
sortedGroups.forEach((group, groupIndex) => {
const components = __classPrivateFieldGet(this, _Schematic_groupedComponents, "f").get(group) || [];
const isLastGroup = groupIndex === sortedGroups.length - 1;
const groupPrefix = isLastGroup ? ' βββ ' : ' βββ ';
process.stdout.write(`${groupPrefix}π¦ ${group}\n`);
components.forEach((component, compIndex) => {
const isLastComponent = compIndex === components.length - 1;
const compPrefix = isLastGroup ? ' ' : ' β ';
const compConnector = isLastComponent ? 'βββ ' : 'βββ ';
process.stdout.write(`${compPrefix}${compConnector}${this.formatComponentDisplay(component)}\n`);
});
});
}
}
_Schematic_options = new WeakMap(), _Schematic_groupedComponents = new WeakMap();