UNPKG

@typecad/typecad

Version:

πŸ€–programmatically πŸ’₯create πŸ›°οΈhardware

529 lines (527 loc) β€’ 21.3 kB
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();