Circuits
Parts are values: arrays, plain functions and .map build repeated blocks, group() names them, and circuit() adds enforced ports and private nets. Refs stay flat; the hierarchy travels as instance paths in the netlist, the findings, inspect and the viewer.
Plain functions and arrays
A part is an ordinary value, so the natural TypeScript works: make parts in a function, keep them in objects and arrays, place and route them in a .map.
// pcb.tsx
import { nets, route } from '@pcbjs/core'
import { R0603, LED0603 } from '@pcbjs/lib'
const { VCC, GND } = nets()
const LEDS = [0, 1, 2, 3].map(i => {
const { A } = nets(`L${i}`)
return { x: 5 + i * 8, R: R0603('330').connect(VCC, A), D: LED0603('green').connect({ A, K: GND }) }
})
const row = ({ x, R, D }: (typeof LEDS)[number]) => [
<R at={`${x} 5`} />,
<D at={`${x} 9`} />,
route(R[2]).X(D.A).width(0.25),
]
// in the board: {LEDS.map(row)}- Each part is numbered like any other, by declaration order: the four resistors are R1 to R4.
- A route made in a helper or in a
.mapcallback inside one is attributed to the helper's call site,pcb.tsx:<line> via pcb.tsx:<line>, in findings and ininspect --lengths. A route written inline in the board's JSX names its own line. - Nothing names the block: a finding says
R3, not which LED it belongs to.group()adds that.
Names with group()
import { group } from '@pcbjs/core'
const PORTS = [0, 1, 2, 3].map(i => {
const { A } = nets(`L${i}`)
return group(`P${i + 1}`, { x: 5 + i * 8, R: R0603('330').connect(VCC, A), D: LED0603('green').connect({ A, K: GND }) })
})group(name, object)makes the object one named instance. Every part in it becomes a member under its key, soPORTS[2].RisP3/R: findings printR3 (P3/R),pcbjs inspect pcb.tsx P3/Rfinds it, and the viewer and the editor show the path.- It returns the same object, typed as it was, with
name,circuit, which is'group', andpathadded and not enumerable. Fields that are not parts, such asx, pass through untouched. - An array field names its parts by index,
P1/LEDS[0]. A group or circuit instance in a field nests,Bank/Sense/R, keeping its own name. - Names are identifiers. The same name twice in one file, a part in two groups, or a field named
name,circuit,pathor$$is aSchematicError. - A group has no ports and no private nets: its parts connect to any net. Refs stay flat and numbered as without it.
layout(group, render)places a group like a circuit instance, see Layouts.
Circuits with ports
circuit() is for a block that should be reusable on its own terms: the caller passes nets for named ports, the parts may reach only those and the instance's private nets, and the editor checks the call.
Defining and calling a circuit
// led-channel.sch.ts
import { circuit, type Ports } from '@pcbjs/core'
import { R0603, LED0603 } from '@pcbjs/lib'
export const ledChannel = circuit('ledChannel', ({ VCC, SW }: Ports<'VCC' | 'SW'>, { nets: { led } }) => ({
R: R0603('330').connect(VCC, led),
D: LED0603('green').connect({ A: led, K: SW }),
}))// board.sch.ts
import { nets } from '@pcbjs/core'
import { ledChannel } from './led-channel.sch'
export const { VCC, SW1 } = nets()
export const CH1 = ledChannel({ VCC, SW: SW1 }, 'CH1')circuit(name, build)returns a circuit.buildtakes the ports and the instance's own nets and returns the parts by key.- The type
Ports<'VCC' | 'SW'>names the ports, so the call is checked in the editor. - The second parameter has
nets, a proxy of nets private to the instance, andlocal, the same proxy. - Every part and nested instance made in the build must come back under a key.
- An instance has its members as typed properties:
CH1.Ris a part andCH1.R[2]a pin handle. It also hasname,circuit,pathsuch as['Power', 'LDO'],portsandnets:CH1.nets.ledis the netCH1/led. - The object form,
circuit('root', { J1: PinHeader(3).connect(…), R1: R0603('1k').connect(…) }), makes one instance of parts made beforehand. It has no ports.group()is the same without the restriction to parts.
Instance names
A variable name cannot be read at run time. An instance takes its name from:
- the second call argument,
buck(ports, 'Buck1'), orbuck.named('Buck1')(ports); - inside another circuit, its key there:
{ LDO: ldo({ … }) }is the instancePower/LDO; - otherwise the circuit name and the lowest free counter,
buck1,buck2.
Names are identifiers. Two instances on one board with the same path are a compile error. Circuits nest and their paths concatenate.
Ports
A port is the only way a net crosses the circuit boundary.
- A part in the build may connect only to the ports and the instance's private nets. Reaching any other net throws a
SchematicErroratconnect. - Every port must be passed. A port left out throws when the build reads it.
- A port name ending in
?, as inPorts<'VIN' | 'EN?'>, may takeNCas well as a net.
Refs
Every part of every instance is numbered per prefix like any other part, in declaration order: an instance takes its place in that order, then its parts go in key order. So CH2.R may be R4 on the board. Each part in the netlist carries path, its instance path, and local, its key. Findings print both, as R4 (CH2/R), and pcbjs inspect pcb.tsx CH2/R or CH2.R finds it.
<PCB refStyle="hierarchical"> names instance parts after their path instead, CH2_R. Parts outside circuits keep the flat numbering.
Layouts
// led-channel.layout.tsx
import { layout, route } from '@pcbjs/core'
import { ledChannel } from './led-channel.sch'
export const LedChannelRow = layout(ledChannel, (g, { at, label }) => (
<g at={at}>
<g.R at="12 0" />
<g.D at="20 0 180deg" />
{route(g.R[2]).Z(g.D.A).width(0.3)}
{label && <text layer="F.SilkS" at="16 -1.4" size={0.8}>{label}</text>}
</g>
), 'LedChannelRow')The board places it with <LedChannelRow g={CH1} at="0 5" label="LED1" />.
layout(circuit, render, name?)returns a component that takes the instance asgplus any props.- The render function places members as
<g.R … />and routes withg.nets.led,g.ports.VCCorg.R[2]. - A circuit may have several layouts, in any file. Pass the name as the third argument; it is what the viewer shows.
- A layout is a plain function too:
{LedChannelRow({ g: CH1, at: '0 5' })}.
Placement rules:
- An instance laid out twice on one board is a compile error naming both sites.
- A part of an instance placed outside the layout that lays it out is a compile error.
- An instance may skip layouts:
<CH1.R at="12 5" />places one part. - An instance on the board with no layout and no part placed is the ERC error
unplaced-circuit. A partly placed instance getsunplaced-partfor each missing part.
Sourcing
source(CH1.R, spec) sets keys on one part of an instance and source(CH1, spec) on every part in it, nested instances included. Factory rules such as source(R0603, '330', spec) reach parts in circuits like any other. See sourcing.md.