diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0592796 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Debug in Chrome", + "url": "http://localhost:8000", + "webRoot": "${workspaceFolder}/public", + "preLaunchTask": "serve" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..a448524 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "serve", + "type": "npm", + "script": "serve", + "isBackground": true, + "presentation": { + "reveal": "silent", + "panel": "dedicated", + "clear": true + }, + "problemMatcher": { + "pattern": { + "regexp": "^(Error|Warning): (.*)$", + "severity": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Generating pinout index", + "endsPattern": "^Serving on http://localhost:8000" + } + } + } + ] +} diff --git a/README.md b/README.md index 05e1148..45dc59a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ Give it a try: **[🚀 Open Web Editor](https://splitbrain.github.io/pinoutleaf/ * **Front & Back Views:** * Automatically generates diagrams for both the front and back sides of the PCB. +* **Chip Diagrams:** + * Besides PCBs, you can also render a chip/IC package (black body, silver leads, pin-1 marker) — handy for documenting a module or IC by itself instead of a whole board. + * Optional automatic or physical pin numbering printed directly on the leads. + ## Screenshot ![Screenshot](screenshot.png) @@ -41,6 +45,7 @@ Give it a try: **[🚀 Open Web Editor](https://splitbrain.github.io/pinoutleaf/ * Only four rows of pins are supported (left, right, top, bottom). * No weird arbitrary layouts. * Only the standard 0.1-inch pin raster is supported (might change in the future). + * Chip diagrams only ever show the front — there's no back side to a chip. ## Alternatives @@ -128,15 +133,51 @@ Defines the title of the diagram: title: "My PCB" ``` +### variant + +There are two diagram variants: `pcb` (the default) and `chip`. + +```yaml +variant: chip +``` + +* `pcb` draws the standard green PCB background (or your own background image, see [image](#image)) and generates both a front and a back view. +* `chip` draws a black IC body with silver leads instead, and only ever generates a front view — flipping is a no-op since a chip has no back side. If a chip has pins on both a vertical side (left/right) and a horizontal side (top/bottom), the body is automatically grown by one empty pin position on every edge so that corner leads don't overlap. + ### size -The size of the PCB is defined in the number of pins (assuming a standard pin raster of 0.1 inch, aka 2.54mm). Everything else is derived from this size. Best to put your board on a breadboard and count. +The size of the PCB or chip is defined in the number of pins (assuming a standard pin raster of 0.1 inch, aka 2.54mm). Everything else is derived from this size. Best to put your board on a breadboard and count. ```yaml width: 4 height: 6 ``` +Both are optional — if omitted, they're calculated automatically from the number of pins you define in the `top`/`bottom` rows (for `width`) and `left`/`right` rows (for `height`). + +### pinNumbers + +For `chip` diagrams, you can have the physical pin number printed on each lead using the `pinNumbers` option. It has no effect on `pcb` diagrams. + +```yaml +pinNumbers: auto +``` + +* `none` (the default) — no pin numbers are shown. +* `auto` — pins are numbered 1-based, starting at the first pin of the `left` row and proceeding down the left side, across the bottom, up the right side, and across the top. Empty pin positions don't count towards the numbering. +* `firstLabel` — instead of calculating a number, the first `label:type` entry of each pin is used as its pin number (parsed as an integer) and is not rendered as a regular label. Useful when your pin numbers aren't sequential or don't start at 1. + +### legend + +The legend isthe box explaining what each color/type means. You can turn it off if you like: + +```yaml +legend: none +``` + +* `auto` (the default) — the legend is shown. +* `none` — the legend is hidden. + ### pins The tool supports up to four rows of pins: `left`, `right`, `top`, and `bottom`. If your board does have a different layout, you're out of luck. diff --git a/public/pinouts/BC849-BC850.yaml b/public/pinouts/BC849-BC850.yaml new file mode 100644 index 0000000..84f75dd --- /dev/null +++ b/public/pinouts/BC849-BC850.yaml @@ -0,0 +1,19 @@ +title: "BC849; BC850 (SOT23)" + +# BC849 and BC850 SOT23 transistors +# Note that this is technically a 2x3 package, but these have "missing" pins. +# You can define an empty (- []) pin to indicate this. +# legend: none, these simple chips don't need a legend +variant: chip +pinNumbers: auto +legend: none + +pins: + left: + - ["base"] + - [] + - ["emitter"] + right: + - [] + - ["collector"] + - [] diff --git a/public/pinouts/ILC6390-91.yaml b/public/pinouts/ILC6390-91.yaml new file mode 100644 index 0000000..d0b43a5 --- /dev/null +++ b/public/pinouts/ILC6390-91.yaml @@ -0,0 +1,21 @@ +title: "ILC6390/91 (SOT-89-5)" + +# ILC6390/91 SOT-89-5 Step-Up PFM Switcher +# A SOT-89-5 package is an example of where we cant use auto-numbering" +# The location of pin nr 5 on the right hand side is actually the same +# as pin nr 2, they are electrically connected at the underside of the package. +# We can use the "firstLabel" option for manually labelling here. +# Note that if you need to manually label 1 pin, you need to label all of them. +variant: chip +pinNumbers: firstLabel +legend: none + +pins: + left: + - [ "1", "N/C"] + - [ "2", "Vout"] + - [ "3", "CE"] + right: + - [ "5", "Vss"] + - [ "2", "Vout"] + - [ "4", "Lx"] diff --git a/public/pinouts/attiny85-qfn20.yaml b/public/pinouts/attiny85-qfn20.yaml new file mode 100644 index 0000000..0c02c8d --- /dev/null +++ b/public/pinouts/attiny85-qfn20.yaml @@ -0,0 +1,51 @@ +title: "ATtiny85 (QFN/MLF-20)" + +# 20-pin QFN/MLF package: 5 pins per side. Only 8 of the 20 pads carry a +# signal - the rest are DNC (Do Not Connect), labeled as such below. +# `pinNumbers: auto` prints the physical pad number next to every pin. +variant: chip +pinNumbers: auto + +pins: + # left column, top to bottom: pads 1-5 + left: + - [ "PB5:gpio", "RESET:control", "ADC0:analog", "dW:control" ] # pad 1 + - [ "PB3:gpio", "XTAL1:control", "ADC3:analog" ] # pad 2 + - [ "DNC:dnc" ] # pad 3 + - [ "DNC:dnc" ] # pad 4 + - [ "PB4:gpio", "XTAL2:control", "ADC2:analog" ] # pad 5 + # bottom row, left to right: pads 6-10 + bottom: + - [ "DNC:dnc" ] # pad 6 + - [ "DNC:dnc" ] # pad 7 + - [ "GND:gnd" ] # pad 8 + - [ "DNC:dnc" ] # pad 9 + - [ "DNC:dnc" ] # pad 10 + # right column, top to bottom: pads 15-11 + right: + - [ "VCC:power" ] # pad 15 + - [ "PB2:gpio", "SCK:spi", "SCL:i2c", "ADC1:analog", "INT0:control" ] # pad 14 + - [ "DNC:dnc" ] # pad 13 + - [ "PB1:gpio", "MISO:spi", "OC0B:pwm" ] # pad 12 + - [ "PB0:gpio", "MOSI:spi", "SDA:i2c", "OC0A:pwm", "AREF:analog" ] # pad 11 + # top row, left to right: pads 20-16 + top: + - [ "DNC:dnc" ] # pad 20 + - [ "DNC:dnc" ] # pad 19 + - [ "DNC:dnc" ] # pad 18 + - [ "DNC:dnc" ] # pad 17 + - [ "DNC:dnc" ] # pad 16 + +types: + control: + label: "Control" + bgcolor: "#637181" + fgcolor: "#ffffff" + pwm: + label: "PWM" + bgcolor: "#e38022" + fgcolor: "#ffffff" + dnc: + label: "Do Not Connect" + bgcolor: "#999999" + fgcolor: "#ffffff" diff --git a/public/pinouts/attiny85.yaml b/public/pinouts/attiny85.yaml new file mode 100644 index 0000000..312627e --- /dev/null +++ b/public/pinouts/attiny85.yaml @@ -0,0 +1,34 @@ +title: "ATtiny85" + +# An 8-pin DIP: two columns of four pins (height). +# Notice that width and height are not given, they are automatically calculated from the number of pins defined. +# Using `variant: chip` draws the black IC body with silver legsn instead of the default PCB rectangle (no background image needed). +variant: chip +pinNumbers: auto + +# Width is auto-calculated as 2, but 3 looks nicer, so we set it. +width: 3 + +pins: + # left column, top to bottom: physical pins 1-4 + left: + - [ "PB5:gpio", "RESET:control", "ADC0:analog", "dW:control" ] + - [ "PB3:gpio", "XTAL1:control", "ADC3:analog" ] + - [ "PB4:gpio", "XTAL2:control", "ADC2:analog" ] + - [ "GND:gnd" ] + # right column, top to bottom: physical pins 8-5 + right: + - [ "VCC:power" ] + - [ "PB2:gpio", "SCK:spi", "SCL:i2c", "ADC1:analog", "INT0:control" ] + - [ "PB1:gpio", "MISO:spi", "OC0B:pwm" ] + - [ "PB0:gpio", "MOSI:spi", "SDA:i2c", "OC0A:pwm", "AREF:analog" ] + +types: + control: + label: "Control" + bgcolor: "#637181" + fgcolor: "#ffffff" + pwm: + label: "PWM" + bgcolor: "#e38022" + fgcolor: "#ffffff" diff --git a/public/pinouts/esp32-wroom-32.yaml b/public/pinouts/esp32-wroom-32.yaml new file mode 100644 index 0000000..4d45f82 --- /dev/null +++ b/public/pinouts/esp32-wroom-32.yaml @@ -0,0 +1,73 @@ +title: "ESP32-WROOM-32 (Module)" + +# The ESP32-WROOM-32 module itself (not a devkit breakout board). +# Pin numbering nd signal names follow: +# https://lastminuteengineers.com/esp32-wroom-32-pinout-reference/ +width: 10 +height: 14 +variant: chip +pinNumbers: auto + +pins: + # left edge, top to bottom: pads 1-14 + left: + - [ "GND:gnd" ] # pad 1 + - [ "3V3:power" ] # pad 2 + - [ "EN:control" ] # pad 3 + - [ "GPIO36:gpio", "VP:analog" ] # pad 4 + - [ "GPIO39:gpio", "VN:analog" ] # pad 5 + - [ "GPIO34:gpio", "ADC1_6:analog" ] # pad 6 + - [ "GPIO35:gpio", "ADC1_7:analog" ] # pad 7 + - [ "GPIO32:gpio", "ADC1_4:analog", "T9:touch" ] # pad 8 + - [ "GPIO33:gpio", "ADC1_5:analog", "T8:touch" ] # pad 9 + - [ "GPIO25:gpio", "ADC2_8:analog", "DAC1:dac" ] # pad 10 + - [ "GPIO26:gpio", "ADC2_9:analog", "DAC2:dac" ] # pad 11 + - [ "GPIO27:gpio", "ADC2_7:analog", "T7:touch" ] # pad 12 + - [ "GPIO14:gpio", "ADC2_6:analog", "T6:touch", "HSPI_CLK:spi" ] # pad 13 + - [ "GPIO12:gpio", "ADC2_5:analog", "T5:touch", "HSPI_MISO:spi" ] # pad 14 + # left edge, left to right: pads 15-24 + bottom: + - [ "GND:gnd" ] # pad 15 + - [ "GPIO13:gpio", "ADC2_4:analog", "T4:touch", "HSPI_MOSI:spi" ] # pad 16 + - [ "GPIO9:flash", "SD2:flash" ] # pad 17 + - [ "GPIO10:flash", "SD3:flash" ] # pad 18 + - [ "GPIO11:flash", "CMD:flash" ] # pad 19 + - [ "GPIO6:flash", "CLK:flash" ] # pad 20 + - [ "GPIO7:flash", "SD0:flash" ] # pad 21 + - [ "GPIO8:flash", "SD1:flash" ] # pad 22 + - [ "GPIO15:gpio", "ADC2_3:analog", "T3:touch", "HSPI_CS:spi" ] # pad 23 + - [ "GPIO2:gpio", "ADC2_2:analog", "LED:control" ] # pad 24 + # right edge, top to bottom: pads 38-25 + right: + - [ "GND:gnd" ] # pad 38 + - [ "GPIO23:gpio", "VSPI_MOSI:spi" ] # pad 37 + - [ "GPIO22:gpio", "SCL:i2c" ] # pad 36 + - [ "GPIO1:gpio", "TX0:uart" ] # pad 35 + - [ "GPIO3:gpio", "RX0:uart" ] # pad 34 + - [ "GPIO21:gpio", "SDA:i2c" ] # pad 33 + - [ "GND:gnd" ] # pad 32 + - [ "GPIO19:gpio", "VSPI_MISO:spi" ] # pad 31 + - [ "GPIO18:gpio", "VSPI_CLK:spi" ] # pad 30 + - [ "GPIO5:gpio", "VSPI_SS:spi" ] # pad 29 + - [ "GPIO17:gpio", "TX2:uart" ] # pad 28 + - [ "GPIO16:gpio", "RX2:uart" ] # pad 27 + - [ "GPIO4:gpio", "ADC2_0:analog", "T0:touch" ] # pad 26 + - [ "GPIO0:gpio", "ADC2_1:analog", "BOOT:control" ] # pad 25 + +types: + control: + label: "Control" + bgcolor: "#637181" + fgcolor: "#ffffff" + dac: + label: "DAC" + bgcolor: "#8e44ad" + fgcolor: "#ffffff" + touch: + label: "Touch" + bgcolor: "#2980b9" + fgcolor: "#ffffff" + flash: + label: "Flash (avoid)" + bgcolor: "#b33939" + fgcolor: "#ffffff" diff --git a/src/Builder.js b/src/Builder.js index 95a4813..2aa530f 100644 --- a/src/Builder.js +++ b/src/Builder.js @@ -1,11 +1,14 @@ import {SvgRoot} from "./elements/SvgRoot.js"; import {Group} from "./elements/Group.js"; import {Circle} from "./elements/Circle.js"; -import {PADDING, PINSIZE, PINSPACE} from "./Constants.js"; +import {Rect} from "./elements/Rect.js"; +import {Text} from "./elements/Text.js"; +import {PADDING, PINSIZE, PINSPACE, CHIPPINLENGTH, CHIPPINWIDTH} from "./Constants.js"; import {PinLabel} from "./components/PinLabel.js"; import {Legend} from "./components/Legend.js"; import {Title} from "./components/Title.js"; import {Pcb} from "./components/Pcb.js"; +import {Chip} from "./components/Chip.js"; import merge from 'lodash.merge'; import {RootGroup} from "./components/RootGroup.js"; @@ -15,9 +18,18 @@ export class Builder { // Diagram Title title: "My PCB", + // Diagram variant: 'pcb' or 'chip' + variant: 'pcb', + + /* PCB Options */ // size in pins - width: 5, - height: 5, + width: NaN, + height: NaN, + + /* Chip Options */ + pinNumbers: 'none', // 'none', 'auto', 'firstLabel' - whether to show pin numbers on the chip + + legend: 'auto', // 'auto', 'none' - whether to show the legend image: { front: { @@ -106,6 +118,22 @@ export class Builder { */ constructor(setup) { this.setup = merge(this.setup, setup); + + const { width, height, pins } = this.setup; + + // Calculate width and height if not provided. + if(!width) + { + const minWidth = !!pins.left + !!pins.right; // Count non-empty vertical sides + this.setup.width = Math.max(pins.top.length, pins.bottom.length, minWidth); + } + + if(!height) + { + const minHeight = !!pins.top + !!pins.bottom; // Count non-empty horizontal sides + this.setup.height = Math.max(pins.left.length, pins.right.length, minHeight); + } + this.normalizePinArrays(); this.isFlipped = false; } @@ -116,11 +144,23 @@ export class Builder { * @returns {SvgRoot} */ build() { + const {title, variant, width, height, legend, image, types, pins} = this.setup; + if(variant === 'chip' && this.isFlipped) { + // Chip diagrams don't have a back side, so if the PCB is flipped, we don't render anything + return null; + } + // Create SVG root const svg = new SvgRoot(); const root = new RootGroup(); svg.append(root); + let background; + if (variant === 'chip') { + background = new Chip(width, height); + } else { + background = new Pcb(width, height, image, this.isFlipped); + } // Create pin rows const pinLayoutGroup = new Group(); @@ -130,45 +170,49 @@ export class Builder { pinLayoutGroup.append(this.createPinRow('bottom', 'under')); root.append(pinLayoutGroup); - // Add the PCB background - const pcb = new Pcb(this.setup.width, this.setup.height, this.setup.image); - pinLayoutGroup.prepend(pcb); + // Add the background + pinLayoutGroup.prepend(background); // Create the title - const title = new Title(this.setup.title + (this.isFlipped ? ' (back)' : ' (front)')); - root.append(title) - - // Create the legend - const legend = new Legend(this.setup.types, this.setup.pins, this.isFlipped); - root.append(legend); + const titleGroup = new Title(title + (this.isFlipped ? ' (back)' : ' (front)')); + root.append(titleGroup); + + let legendGroup = null; + if(legend !== 'none') { + // Create the legend + legendGroup = new Legend(types, pins, this.isFlipped); + root.append(legendGroup); + } - // Get bounding box of the main pin layout - const pinLayoutBBox = pinLayoutGroup.getBoundingBox(); - const legendBBox = legend.getBoundingBox(); - const titleBBox = title.getBoundingBox(); + const rootBBox = root.getBoundingBox(); // Position legend to the bottom aligned to the right/left of the pin layout with padding - let rootBBox = root.getBoundingBox(); - if(this.isFlipped) { - // FIXME - // it's hacky: - // rect returns the visual bounding box that includes the stroke width - // for alignment we need to adjust for the stroke width, thus the +10 - // see Rect.getBoundingBox() - legend.setTranslate( - pinLayoutBBox.x - legendBBox.width + 10 - (PADDING * 3), - rootBBox.y + rootBBox.height - legendBBox.height - ); - } else { - legend.setTranslate( - pinLayoutBBox.x + pinLayoutBBox.width + (PADDING * 3), - rootBBox.y + rootBBox.height - legendBBox.height - ); + if(legendGroup) { + const pinLayoutBBox = pinLayoutGroup.getBoundingBox(); + const legendBBox = legendGroup.getBoundingBox(); + + if(this.isFlipped) { + // FIXME + // it's hacky: + // rect returns the visual bounding box that includes the stroke width + // for alignment we need to adjust for the stroke width, thus the +10 + // see Rect.getBoundingBox() + legendGroup.setTranslate( + pinLayoutBBox.x - legendBBox.width + 10 - (PADDING * 3), + rootBBox.y + rootBBox.height - legendBBox.height + ); + } else { + legendGroup.setTranslate( + pinLayoutBBox.x + pinLayoutBBox.width + (PADDING * 3), + rootBBox.y + rootBBox.height - legendBBox.height + ); + } } // Position title at the top, left aligned - rootBBox = root.getBoundingBox(); - title.setTranslate(rootBBox.x, rootBBox.y - PADDING - titleBBox.height); + const titleBBox = titleGroup.getBoundingBox(); + const rootBBoxWithLegend = root.getBoundingBox(); + titleGroup.setTranslate(rootBBoxWithLegend.x, rootBBoxWithLegend.y - PADDING - titleBBox.height); // Update SVG bounds to include everything root.reframe(); @@ -183,31 +227,36 @@ export class Builder { * reverses the order of the top and bottom pins and swaps the front and back images. */ flip() { + const { variant, image, offsets, pins } = this.setup; this.isFlipped = !this.isFlipped; + if(variant=== 'chip') { + return; // No need to flip chip diagrams, we only want to flip PCB diagrams + } + // Swap left and right pins - const tempLeftPins = this.setup.pins.left; - this.setup.pins.left = this.setup.pins.right; - this.setup.pins.right = tempLeftPins; + const tempLeftPins = pins.left; + pins.left = pins.right; + pins.right = tempLeftPins; // Swap left and right offsets - const tempLeftOffset = this.setup.offsets.left; - this.setup.offsets.left = this.setup.offsets.right; - this.setup.offsets.right = tempLeftOffset; + const tempLeftOffset = offsets.left; + offsets.left = offsets.right; + offsets.right = tempLeftOffset; // Reverse top and bottom pins - if (this.setup.pins.top) { - this.setup.pins.top.reverse(); + if (pins.top) { + pins.top.reverse(); } - if (this.setup.pins.bottom) { - this.setup.pins.bottom.reverse(); + if (pins.bottom) { + pins.bottom.reverse(); } // Swap front and back images - if (this.setup.image) { - const tempFrontImage = this.setup.image.front; - this.setup.image.front = this.setup.image.back; - this.setup.image.back = tempFrontImage; + if (image) { + const tempFrontImage = image.front; + image.front = image.back; + image.back = tempFrontImage; } } @@ -215,7 +264,7 @@ export class Builder { * Ensures pin arrays match the defined width and height. */ normalizePinArrays() { - const {width, height, pins} = this.setup; + const {variant, width, height, pins} = this.setup; const adjustPinArray = (arrayName, targetLength) => { const currentArray = pins[arrayName]; @@ -233,12 +282,42 @@ export class Builder { } else if (currentLength > targetLength) { // Truncate the array console.warn( - `Builder: Pin array '${arrayName}' has ${currentLength} elements, but expected ${targetLength}. + `Builder: Pin array '${arrayName}' has ${currentLength} elements, but expected ${targetLength}. Truncating excess elements.` ); pins[arrayName] = currentArray.slice(0, targetLength); } }; + + /** + * For chip diagrams that have pins on both a vertical side (left/right) and a + * horizontal side (top/bottom), the corner pin of a vertical row lands on the + * exact same grid position as the corner pin of a horizontal row, causing their + * labels to overlap. To avoid this, grow the chip by one empty column and one + * empty row on every side, shifting all existing pins inward so the corners are + * left empty (unrendered) instead of shared between two labels. + */ + const avoidChipCornerOverlap = () => { + const { width, height, pins } = this.setup; + + const hasPins = (row) => row.some(pin => pin && pin.length); + const hasVertical = hasPins(pins.left) || hasPins(pins.right); + const hasHorizontal = hasPins(pins.top) || hasPins(pins.bottom); + if (!hasVertical || !hasHorizontal) { + return; + } + + const emptyPin = () => []; + const grow = (row) => [emptyPin(), ...row, emptyPin()]; + + pins.left = grow(pins.left); + pins.right = grow(pins.right); + pins.top = grow(pins.top); + pins.bottom = grow(pins.bottom); + + this.setup.width += 2; + this.setup.height += 2; + } // Adjust vertical arrays (left, right) based on height adjustPinArray('left', height); @@ -247,6 +326,140 @@ export class Builder { // Adjust horizontal arrays (top, bottom) based on width adjustPinArray('top', width); adjustPinArray('bottom', width); + + if (variant === 'chip') { + avoidChipCornerOverlap(); + } + } + + + /** + * Determines the physical pin number for a pin at a given row/index. + * + * Pin 1 is the first pin in the left column. Numbering then proceeds down the + * left side, across the bottom, up the right side, and across the top. + * Empty pin locations don't count towards the pin index (since no pin is drawn on those locations). + * + * @param {string} row The row identifier ('left', 'right', 'top', 'bottom') + * @param {number} index The index of the pin within its row + * @returns {number} The 1-based pin number. + */ + calculateAutoPinNumber(row, index) { + const {pins} = this.setup; + + // Count the number of non-empty pins in each row + const leftPins = pins.left.filter(pin => pin && pin.length); + const bottomPins = pins.bottom.filter(pin => pin && pin.length); + const rightPins = pins.right.filter(pin => pin && pin.length); + const topPins = pins.top.filter(pin => pin && pin.length); + + const nrOfLeftPins = leftPins.length; + const nrOfBottomPins = bottomPins.length; + const nrOfRightPins = rightPins.length; + const nrOfTopPins = topPins.length; + + function calculatePinNumberInRow(pinArray, reverse = false) { + let idx = index; + if (reverse) { + // Start counting from the end of the array for reverse order + idx = -index-1; + } + return pinArray.slice(0, idx).filter(pin => pin && pin.length).length; + } + + let pinNumber; + switch (row) { + case 'left': + pinNumber = calculatePinNumberInRow(pins.left); + break; + case 'bottom': + pinNumber = nrOfLeftPins + calculatePinNumberInRow(pins.bottom); + break; + case 'right': + // The right side is numbered in reverse order, so we need to count from the end of the array + pinNumber = nrOfLeftPins + nrOfBottomPins + calculatePinNumberInRow(pins.right, true); + break; + case 'top': + // The top side is also numbered in reverse order, so we need to count from the end of the array + pinNumber = nrOfLeftPins + nrOfBottomPins + nrOfRightPins + calculatePinNumberInRow(pins.top, true); + break; + default: + throw new Error(`Invalid row: ${row}`); + } + + return pinNumber + 1; // Convert to 1-based numbering + } + + getPinNumber(row, index) { + switch (this.setup.pinNumbers) { + case 'none': + return null; + case 'auto': + return this.calculateAutoPinNumber(row, index); + case 'firstLabel': + const pinLabels = this.setup.pins[row][index]; + if (!pinLabels || !pinLabels.length) { + return null; + } + const firstLabel = pinLabels[0]; + const number = parseInt(firstLabel, 10); + return isNaN(number) ? null : number; + default: + throw new Error(`Invalid pinNumbers option: ${this.setup.pinNumbers}`); + } + } + + /** + * Creates the pin number label, printed in white inside the chip body next to + * the pin's lead, sized so its height roughly matches the lead's width. + * + * @param {string} row The row identifier ('left', 'right', 'top', 'bottom') + * @param {number} index The index of the pin within its row + * @param {{x: number, y: number}} pos The pin's position + * @param {{left: number, right: number, top: number, bottom: number}} body The chip body bounds + * @returns {Text} The configured Text element. + */ + createChipPinNumber(row, index, pos, body) { + + const { pins } = this.setup; + + if ( !pins[row][index]){ + return null; + } + + const number = this.getPinNumber(row, index); + const fontSize = CHIPPINWIDTH; + const inset = CHIPPINWIDTH; + + const attrs = { + fill: '#ffffff', + 'font-size': fontSize, + }; + + let x, y; + switch (row) { + case 'left': + x = body.left + inset; + y = pos.y - fontSize / 2; + break; + case 'right': + x = body.right - inset; + y = pos.y - fontSize / 2; + attrs['text-anchor'] = 'end'; + break; + case 'top': + x = pos.x; + y = body.top + inset; + attrs['text-anchor'] = 'middle'; + break; + case 'bottom': + x = pos.x; + y = body.bottom - inset - fontSize; + attrs['text-anchor'] = 'middle'; + break; + } + + return new Text(x, y, String(number), attrs); } /** @@ -257,13 +470,54 @@ export class Builder { * @returns {Group} A group containing the pin and its labels */ createPinWithLabels(row, pinIndex, alignment) { + const { variant, pinNumbers, pins, types } = this.setup; const group = new Group(); const pos = this.pinPosition(row, pinIndex); - const pinElement = new Circle(pos.x, pos.y, PINSIZE, 'gold'); + + let pinElement; + let pinNumberElement = null; + switch (variant) { + case 'chip': { + const body = this.chipBodyBounds(); + switch (row) { + case 'left': + pinElement = new Rect(body.left - CHIPPINLENGTH, pos.y - CHIPPINWIDTH / 2, CHIPPINLENGTH, CHIPPINWIDTH, { fill: 'silver' }); + break; + case 'right': + pinElement = new Rect(body.right, pos.y - CHIPPINWIDTH / 2, CHIPPINLENGTH, CHIPPINWIDTH, { fill: 'silver' }); + break; + case 'top': + pinElement = new Rect(pos.x - CHIPPINWIDTH / 2, body.top - CHIPPINLENGTH, CHIPPINWIDTH, CHIPPINLENGTH, { fill: 'silver' }); + break; + case 'bottom': + pinElement = new Rect(pos.x - CHIPPINWIDTH / 2, body.bottom, CHIPPINWIDTH, CHIPPINLENGTH, { fill: 'silver' }); + break; + } + if (pinNumbers != 'none') { + pinNumberElement = this.createChipPinNumber(row, pinIndex, pos, body); + } + break; + } + case 'pcb': + default: + pinElement = new Circle(pos.x, pos.y, PINSIZE, 'gold'); + break; + } + group.append(pinElement); + if (pinNumberElement) { + group.append(pinNumberElement); + } let last = pinElement; - this.setup.pins[row][pinIndex].forEach((pindata, index) => { + pins[row][pinIndex].forEach((pindata, index) => { + + if(pinNumbers === 'firstLabel' && index === 0) { + // Skip the first label if pinNumbers is set to 'firstLabel' + // since the first label is already used as the pin number. + return; + } + const [text, type] = pindata.split(':'); const {bgcolor, fgcolor} = this.setup.types[type] || this.setup.types.default; @@ -279,6 +533,22 @@ export class Builder { return group; } + /** + * Calculates the edges of the chip body (the Pcb background rect) in the pin + * layout's coordinate space, so chip leads can be aligned flush against it. + * + * @returns {{left: number, right: number, top: number, bottom: number}} + */ + chipBodyBounds() { + const bodyPadding = PINSPACE / 2; // matches Pcb's default padding + return { + left: -bodyPadding, + right: (this.setup.width - 1) * PINSPACE + bodyPadding, + top: -bodyPadding, + bottom: (this.setup.height - 1) * PINSPACE + bodyPadding, + }; + } + /** * Creates a row of pins * @param {string} row The row identifier ('left', 'right', 'top', 'bottom') diff --git a/src/Constants.js b/src/Constants.js index 4078e84..718a1ea 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -3,11 +3,15 @@ const PINSPACE = 254; // 2.54mm The space between pin holes (standard 0.1 inch) const PADDING = 100; // 1mm Padding between labels const FONTSIZE = 150; // Font size for labels const CORNERS = 30; // Corner radius +const CHIPPINLENGTH = 150; // 1.5mm The length of a chip lead/pin +const CHIPPINWIDTH = 90; // 0.9mm The width (thickness) of a chip lead/pin export { PINSIZE, PINSPACE, PADDING, FONTSIZE, - CORNERS + CORNERS, + CHIPPINLENGTH, + CHIPPINWIDTH } diff --git a/src/Editor.js b/src/Editor.js index 77f8e8a..3741889 100644 --- a/src/Editor.js +++ b/src/Editor.js @@ -38,7 +38,8 @@ export class Editor { highlightActiveLine: true, highlightSelectedWord: false, useWrapMode: true, - tabSize: 2 + tabSize: 2, + useWorker: false }); this.initializeEditor(); @@ -158,13 +159,13 @@ export class Editor { const builder = new Builder(setup); - const front = builder.build().render(window.document); + const front = builder.build(); builder.flip(); - const back = builder.build().render(window.document); + const back = builder.build(); this.output.innerHTML = ''; - this.output.appendChild(front); - this.output.appendChild(back); + if(front) this.output.appendChild(front.render(window.document)); + if(back) this.output.appendChild(back.render(window.document)); } /** diff --git a/src/cli.js b/src/cli.js index 97d9a4b..ecdf24e 100644 --- a/src/cli.js +++ b/src/cli.js @@ -73,14 +73,20 @@ async function processFile(file) { const builder = new Builder(setup); const window = createWindow(); - const frontSvgContent = builder.build().render(window.document).outerHTML; - await writeFile(outputBase + '.front.svg', frontSvgContent, 'utf8'); - console.info(outputBase + '.front.svg written'); + const frontSvg = builder.build(); + if(frontSvg) { + const frontSvgContent = frontSvg.render(window.document).outerHTML; + await writeFile(outputBase + '.front.svg', frontSvgContent, 'utf8'); + console.info(outputBase + '.front.svg written'); + } builder.flip(); - const backSvgContent = builder.build().render(window.document).outerHTML; - await writeFile(outputBase + '.back.svg', backSvgContent, 'utf8'); - console.info(outputBase + '.back.svg written'); + const backSvg = builder.build(); + if(backSvg) { + const backSvgContent = backSvg.render(window.document).outerHTML; + await writeFile(outputBase + '.back.svg', backSvgContent, 'utf8'); + console.info(outputBase + '.back.svg written'); + } } /** diff --git a/src/components/Chip.js b/src/components/Chip.js new file mode 100644 index 0000000..35c346b --- /dev/null +++ b/src/components/Chip.js @@ -0,0 +1,70 @@ +import {Group} from '../elements/Group.js'; +import {Rect} from '../elements/Rect.js'; +import {Circle} from '../elements/Circle.js'; +import {Image} from '../elements/Image.js'; // Import the new Image element +import {CORNERS, PINSIZE, PINSPACE} from '../Constants.js'; + +/** + * Represents the Chip background. + */ +export class Chip extends Group { + + /** + * Creates a Chip component. + * @param {number} widthInPins - The width of the chip in number of pins. + * @param {number} heightInPins - The height of the chip in number of pins. + * @param {object} [options={}] - Optional configuration. + * @param {string} [options.fill] - Background color of the Chip. + * @param {number} [options.padding=PADDING * 2] - Padding around the pin area. + */ + constructor(widthInPins, heightInPins, options = {}) { + super(); + + const { + fill = '#000000', // Default chip color + padding = PINSPACE / 2, // Padding around the pin area + } = options; + + // Calculate the dimensions based on pin layout + const chipWidth = (widthInPins - 1) * PINSPACE + padding * 2; + const chipHeight = (heightInPins - 1) * PINSPACE + padding * 2; + + // Calculate position to center the pin area within the padding + const chipX = -padding; + const chipY = -padding; + + this.append(this.createRectBackground(chipX, chipY, chipWidth, chipHeight, fill)); + this.append(this.createChipMarker(chipX, chipY)); + } + + /** + * Creates the pin-1 indicator marker for chip components. + * @private + * @param {number} pcbX - X position of the PCB background. + * @param {number} pcbY - Y position of the PCB background. + * @returns {Circle} The configured Circle element. + */ + createChipMarker(pcbX, pcbY) { + const markerRadius = PINSIZE / 2; + const markerOffset = CORNERS + markerRadius; + return new Circle(pcbX + markerOffset, pcbY + markerOffset, markerRadius, 'lightgrey'); + } + + /** + * Creates the background rectangle element. + * @private + * @param {number} pcbX - X position. + * @param {number} pcbY - Y position. + * @param {number} pcbWidth - Width. + * @param {number} pcbHeight - Height. + * @param {string} fill - Fill color. + * @returns {Rect} The configured Rect element. + */ + createRectBackground(pcbX, pcbY, pcbWidth, pcbHeight, fill) { + return new Rect(pcbX, pcbY, pcbWidth, pcbHeight, { + fill: fill, + rx: CORNERS, + ry: CORNERS, + }); + } +} diff --git a/src/elements/Text.js b/src/elements/Text.js index 9bac6cb..8b6a6c7 100644 --- a/src/elements/Text.js +++ b/src/elements/Text.js @@ -6,11 +6,11 @@ export class Text extends BaseElement { super('text', { x, y, - ...attrs, // align on top left corner 'dominant-baseline': 'hanging', 'font-family': 'Roboto Mono', 'font-size': FONTSIZE, + ...attrs, }); this.text = text; }