Skip to content

3D view ​

What the 3D view draws, the generated part models, model files and their cache, and how the geometry is built. Everything is built from the compiled board, so a click in 3D names the same item as in 2D. Fetching and fitting a model file is in models.md.

What it draws ​

  • Every pad, track, via, zone, graphic and text is its own mesh, built from items in the site data: polygon rings in board mm with id, kind, layers, net, ref and drill.
  • Copper sits at the stackup's copper thickness on its own layer, inner layers at their stackup depth.
  • Every plated hole has its barrel, a tube along a slot. A drill cuts the board and the copper and silk it passes, so a track stops at the barrel; a slot cuts a stadium of its width and length along its axis, as the drill file routes it. NPTH holes cut the mask too, with no copper.
  • The solder mask is a slab with the pad openings cut out plus a skin over each outer copper item's part outside the openings, so tracks stand out and copper in an opening shows the finish. Mask and silk take their colours from the stackup; fab names such as Green or Black map to matching shades, and any CSS colour works.
  • Every part gets a generated model from its model descriptor, or the meshes of a model file, see below.
  • Another entry's board placed with reference() shows as one translucent group under References in the layer list, see references.md.

Interaction ​

  • Drag to orbit, scroll to zoom. Hover names the part and pad or the net, the kind and the layer, and lights up the net.
  • A click works as in 2D: a pad, via or track pins its net, a part body selects the part, empty space clears. It opens the code as editor.md describes.
  • The corner buttons frame the whole board, the top, the bottom and an iso view, then toggle the grid, the focus fade and the exploded view. A frame takes the board, its parts, references and exploded stack, never the grid. The ground grid takes the 2D grid's tones and steps, see viewer.md, and fades into the canvas far from the board.
  • Exploded, the layers move apart in stackup order. A slider hanging under the explode button sets the gap from 0 to 15 mm, 4 by default, and a callout names each layer with its thickness and material.
  • Turning explode on or off fits the view to the open stack or the assembled board, the camera moving with the layers and keeping its direction. Exploded, the fit keeps room on the right for the callout column, as wide as its longest label. Dragging the slider keeps the framing; the fit button frames what is shown. With reduced motion both jump to the end.
  • The callouts stand in one column right of the rightmost layer anchor on screen, each a plain leader from its layer's rightmost outline point, so the leaders fan out as the board orbits. A label is text in its layer's colour, semi-transparent at rest.
  • The parts and silk planes of each side draw the board outline, cutouts included, in thin lines with a faint grid inside at the ground grid's step, the silk in its callout colour.
  • Hovering a layer, its callout or its slab makes that callout opaque and dims every other layer and callout until the pointer leaves.
  • A selected part, pad or group or a pinned net fades the rest, see viewer.md. While it does, a popover under the focus fade button sets how the rest is drawn, remembered by the browser: its opacity, 25% by default, for substrate, mask, copper and bodies; Monochrome, greying it so the focus is the only colour; Shaded, or Edges for edges sharper than 30° and no faces; Grouped, the default, drawing it as one body so its hidden faces stay hidden, or Stacked, every surface showing through. The focus stays opaque; leaving it restores the board.
  • A layer hidden in the rail is hidden in 3D too.

Models ​

A footprint's model prop is a generated descriptor or a model file:

DescriptorUsed by
{ kind: 'chip', part: 'R' | 'C' | 'L' | 'D', size, marking? }chip passives; a resistor prints its code from resistorCode, such as 331
{ kind: 'led', size, color?, lens? }chip LEDs; the colour comes from a value such as green
{ kind: 'header', pins, rows, pitch, pinLength? }PinHeader
{ kind: 'sot23', pins: 3 }three-pin SOT-23
{ kind: 'ic', w, h, height, color? }IC packages: SOT-223, SOIC, TSSOP, QFN, QFP
{ kind: 'box', w, h, height, reason? }no model: a footprint of your own without one, or a fetched model that did not download
{ file, offset?, rotate?, scale?, fallback? }a STEP, glTF or WRL file, below; switches, crystals, connectors, modules and the battery holder of the standard library fetch theirs on first use
  • A part with no model file and no generated body, a box or no model at all, is never drawn solid: its body box, or the Fab outline and height, is a dashed amber outline with faint faces, its ref and no model on top, in the view and in pcbjs shot alike. A referenced board leaves it out. pcbjs model check, inspect and README-fab count these parts as no model, the --json report as models.missing. A bare footprint, a hole, test point, pogo pad, fiducial or mounting hole as layout.md Silk refs defines it, defaults to height 0 and so draws nothing unless the part sets a height or a model.
  • A box with zero size or height draws nothing and counts as no body, as for test pads and mounting holes.
  • The resistor tolerance comes from a tolerance field, fields={{ tolerance: '1%' }}. marking overrides the code.
  • Identical parts share geometry and materials. The generators are in packages/viewer/src/models.

Model files ​

ts
import { R0603, PinHeader } from '@pcbjs/lib'

const R1 = R0603({ value: '5.1k', model: { file: './models/R_0603_1608Metric.step' } })
const J2 = PinHeader(4, { model: { file: './models/PinHeader_1x04_P2.54mm_Vertical.wrl', offset: [0, 0, 0], rotate: [0, 0, 0] } })
  • file is STEP or STP, glTF or GLB, or WRL, the format told by the extension. step is an older name for it and still works.
  • file is relative to the file that holds the model literal, as an import is: the part call for a part's option, the file of the <footprint> element for a footprint's prop. A footprint in lib/parts.tsx with its model in the project's models/ names ../models/x.step, and a footprint shared from outside the project names the file beside it. An absolute path or a file: URL is taken as it is. kind: 'step' may be written but is not needed.
  • The conventions are KiCad's, so a (model ...) line of a .kicad_mod maps one to one. The model frame is millimetres, origin at the footprint origin, x along the footprint x, y along the footprint -y, z up from the board face. offset is in mm, rotate in degrees about x, y and z, scale a factor per axis. The model is scaled, then turned by the negated angles about x, then y, then z, then moved by the offset, as KiCad does it.
  • Units as read: STEP carries its own unit and comes out in mm; glTF is metres with y up, turned to z up; WRL is KiCad's tenth of an inch, 2.54 mm.
  • The part then sits like any other: at the footprint position and rotation, and on the back turned over about its y axis under the bottom copper.
  • The standard library keeps its generated model, or its body outline, as fallback; a footprint of your own may give one. The view draws the fallback, or the Fab box, until the mesh arrives, then swaps the mesh in. Explode, fade, highlight and picking work on it as on a generated model.
  • A file that is not there is the ERC warning model-missing with the path, and the view draws the fallback with one console warning.
  • A model whose box reaches more than 1 mm past the courtyard or is under a fifth of it, whose top is under the board face, whose surface-mount body reaches more than 0.2 mm into the board, or whose height is more than 1.6 times off the footprint's height, gets a thin amber outline in 3D and a line under model in the Part panel naming the problem. A height off while a side matches it reads lying on its side?.

The cli converts each file once per content with @pcbjs/step, in Node only: STEP through OpenCascade compiled to WASM by occt-import-js, glTF, GLB and WRL through small readers of its own. Curved STEP faces are triangulated within 0.02 mm and 0.3 rad, so a 2 mm pin has at least 21 sides; colours come from the file, one mesh per colour.

  • The mesh is cached at .pcbjs/models/<hash>.bin, <hash> the sha256 of the file content and the conversion options, shared by every footprint, board and process that uses the file.
  • The model box also gives a footprint without a height its body height in the reference-collision check and the default gap of a mate, see references.md.
  • The site data has step: { file, mesh, transform, bounds, courtyard, issue } per footprint: mesh the hash, absent when the file is missing, bounds the mesh extent, issue the courtyard problem if any. The compile converts a new file once to fill them.
  • Dev serves the mesh at /__pcbjs/model/<hash>, converting on first request. pcbjs build copies the used meshes to models/<hash>.bin in the site. The view fetches each once per page.
  • occt-import-js and OpenCascade are LGPL-2.1, loaded as an unmodified library.

Fetched on first use ​

A model whose file is a source rather than a path is downloaded the first time a compile needs it:

SourceFrom
kicad:<Lib>.3dshapes/<Name>.stepan installed KiCad library when there is one, else KiCad's packages3D on GitLab at tag 10.0.7, one file by its raw URL
lcsc:C123EasyEDA's model of the LCSC part, as pcbjs model add lcsc:C123
https://…the URL as it is
  • A ${KICAD9_3DMODEL_DIR}/… path, which pcbjs import writes and kicadModel() keeps when no KiCad is installed, is the same as kicad:….
  • The file lands in .pcbjs/models/fetched/, under kicad-10.0.7/, lcsc/ or the host name, with one line per file in .pcbjs/models/fetched/LICENSES.md: the URL, the licence and the date. Nothing is fetched again while the file is there; delete it to fetch anew.
  • The compile downloads in a child process, once per file per process, so dev, check, build and shot all fetch. The site data and model check name the cached file like any other.
  • Offline, or when a download fails, the part keeps its fallback, the dashed no model outline, and the compile prints one line per board naming the parts: pcbjs: Blinky: SW1, Y1 show dashed outlines, the model did not download (…); run pcbjs model fetch. The board still compiles and checks clean. PCBJS_OFFLINE=1 skips every download.
  • pcbjs model fetch [entry...] compiles the boards, downloads what is missing and lists any part still waiting. model check flags those parts with that fix and ends with a line such as standard library: 24 parts, 24 with fetched models, 0 generated, 0 with no body. pcbjs doctor shows the installed KiCad library and how many files are cached.
  • Nothing is copied into a package: @pcbjs/lib holds only the source and the alignment of each model, LICENSES.md has the terms.

Bringing your own model ​

pcbjs model add, align and check fetch a model file, fit it to a placed part and list every part's model; models.md has the commands, where to find models and their licences.

How it builds ​

  • The extrusions are geometry jobs, packages/viewer/src/scene/geometry.ts, run by a small pool of web workers in batches: small copper and silk first, then mask skins, then pours, then slabs with many holes.
  • The first frame has the board slab, the bodies, the barrels and every cached geometry; each batch adds its meshes as it lands.
  • A pour with many holes is triangulated per cell; a slab with many holes shows whole first and swaps in its cut geometry from a worker.
  • packages/export/src/items.ts cuts in Node: each item less the drills near it, and covered, its part under the mask.
  • Geometries are cached by a hash of their rings; a small board builds everything at once on the main thread.

MIT licensed. A proof of concept, pre 0.1.