Skip to content

Latest commit

 

History

History
292 lines (222 loc) · 10.2 KB

File metadata and controls

292 lines (222 loc) · 10.2 KB

@tscircuit/3d-viewer

Note

We are working on a new vanilla THREE version of the 3d viewer, it's on the v01 branch

A 3D printed circuit board viewer for Circuit JSON and tscircuit

npm version License: MIT

Documentation · Website · Twitter · Discord · Quickstart · Online Playground

image

Features

  • 3D visualization of PCB layouts
  • Interactive camera controls (pan, zoom, rotate)
  • Support for various PCB components (resistors, capacitors, Chips, etc.)
  • Customizable board and component rendering

Installation

npm install @tscircuit/3d-viewer

Usage

Basic Example

import React from "react"
import { CadViewer } from "@tscircuit/3d-viewer"

const MyPCBViewer = () => {
  return (
    <CadViewer>
      <board width="20mm" height="20mm">
        <resistor
          name="R1"
          footprint="0805"
          resistance="10k"
          pcbX={5}
          pcbY={5}
        />
        <capacitor
          name="C1"
          footprint="0603"
          capacitance="1uF"
          pcbX={-4}
          pcbY={0}
        />
      </board>
    </CadViewer>
  )
}

export default MyPCBViewer

Using with circuitJson Data

import React from "react"
import { CadViewer } from "@tscircuit/3d-viewer"
import mycircuitJsonData from "./mycircuitJsonpData.json"

const MyPCBViewer = () => {
  return <CadViewer circuitJson={mycircuitJsonData} />
}

export default MyPCBViewer

Converting to SVG (Node.js)

When using the SVG converter in Node.js environments, you'll need to provide JSDOM:

import { JSDOM } from 'jsdom'
import { convert3dCircuitToSvg } from '@tscircuit/3d-viewer/3d'
import { applyJsdomShim } from '@tscircuit/3d-viewer/utils'

// Setup JSDOM environment
const dom = new JSDOM()
applyJsdomShim(dom)

// Convert circuit to SVG
const options = {
  width: 800,
  height: 600,
  backgroundColor: "#ffffff",
  padding: 20,
  zoom: 50,
  camera: {
    position: { x: 0, y: 0, z: 100 },
    lookAt: { x: 0, y: 0, z: 0 }
  }
}

const svgString = await convert3dCircuitToSvg(circuitJson, options)

The convert3dCircuitToSvg function accepts the following options:

  • width: Width of the output SVG (default: 800)
  • height: Height of the output SVG (default: 600)
  • backgroundColor: Background color in hex format (default: "#ffffff")
  • padding: Padding around the board (default: 20)
  • zoom: Zoom level (default: 1.5)
  • camera: Camera position and lookAt configuration
    • position: {x, y, z} coordinates for camera position
    • lookAt: {x, y, z} coordinates for camera target

API Reference

<CadViewer>

Main component for rendering the 3D PCB viewer.

Props:

  • circuit-json: (optional) An array of AnyCircuitElement objects representing the PCB layout.
  • children: (optional) React children elements describing the PCB layout (alternative to using circuit-json).
  • resolveStaticAsset: (optional) Function that receives each component model URL (obj, wrl, stl, gltf, glb, step) and returns the resolved URL to load.

<board>

Defines the PCB board dimensions.

Props:

  • width: Width of the board (e.g., "20mm").
  • height: Height of the board (e.g., "20mm").

Component Elements

Various component elements can be used as children of the <board> element:

  • <resistor>
  • <capacitor>
  • <chip>
  • <bug> (for ICs)

Each component has specific props for defining its characteristics and position on the board.

Advanced Usage

Non-blocking renderer comparison diagnostics

The Diagnostics/Renderer Parity stories compare the actual viewer with circuit-json-to-gltf using the same Circuit JSON and local model assets:

bun run storybook:comparisons

This opens Diagnostics/Renderer Parity / X Rotation directly instead of restoring an unrelated story. The ordinary bun run storybook entry point is unchanged.

Each comparison story automatically displays both oblique and side geometry once its model geometry loads. Both panels remain visible, with the exact unlit/no-texture PNGs, edge maps, red/cyan overlays, and metrics used by the browser tests. There are no comparison buttons. Click an image to download its full-resolution PNG. Bottom-layer fixtures use cameras below the PCB.

circuit-json-to-gltf is a development-only dependency. A Bun preparation step generates GLBs and records circuit-json-to-gltf failures; it is not imported into the viewer's production bundle or the browser story. Model files are local, not fetched from ModelCDN during the tests.

build-storybook also prepares the fixture data, so the existing vercel-build entry point publishes working comparison stories. Vercel hosts those static stories; Playwright diagnostics run separately in GitHub Actions.

The browser suite has two separate roles:

# Blocking tests of the matcher itself and deliberate browser mutations
bun test ./tests/geometry-edge-*.test.ts ./tests/renderer-diagnostic-result.test.ts ./tests/renderer-comparison-*.test.ts ./tests/renderer-usb-mounting.test.ts
bun run test:renderer-calibration

# Diagnostic comparisons: failures are reported but do not gate a completed run
bun run test:renderer-comparisons
bunx --no-install playwright show-report

Install the test browser with bunx playwright install chromium if Playwright reports that its Chromium executable is missing.

CI uses Ubuntu, Node 22, Bun 1.3.14 and the Chromium version paired with the pinned Playwright dependency. playwright install --with-deps chromium installs the Linux browser libraries; captures use software WebGL and require no physical GPU. Playwright executes under Node even when launched by a Bun package script.

A local Node 26 module.register() deprecation warning is not a test failure; Node 22 is the CI runtime. Vite messages about missing dependency source maps (for example manifold-3d/lib/wasm.js.map) concern debugger metadata, not a missing JavaScript or WebAssembly module. A run ending in passed completed successfully. Runtime/model-loading errors are still surfaced normally; the harness does not blanket-suppress logs.

Actual comparisons are ordinary failing Playwright assertions, not skipped or automatically approved baselines. The runner retains those failures and their artifacts but returns success after a completed diagnostic report, making it non-blocking for local gate runners too. Missing reports, global runner errors, and interrupted runs still fail. For strict exit behavior, invoke bunx playwright test --project=diagnostics directly.

The CI comparison step additionally has continue-on-error: true. Dependency setup, matcher/runner unit tests, and browser calibration remain blocking. The workflow always uploads the HTML report, geometry images, edge maps, diff overlays, metadata, and failure traces.

The geometry pass isolates the studied CAD component, preserves its actual world placement, and uses neutral unlit surfaces rather than textures/shadows. The exported GLB receives only the fixed frame conversion back to project coordinates: P = (-G.x, G.z, G.y). Both sides use the same orthographic camera and viewport. There is no recentering, image registration, or per-renderer fit.

The matcher checks edge coverage in both directions with a 1.5-pixel tolerance and at most 1% unmatched edges on each side. It does not average differences over the whole PCB image. Browser calibration requires identical geometry to match and deliberate X-sign, rotation-order, and 0.2248885 mm origin errors to be rejected. Pixel agreement is not a substitute for the existing numerical placement tests, and it is not a test of material or normal appearance. The calibration case has no exported GLB and compares viewer geometry only, so browser-side exporter failures remain in the non-blocking diagnostic stage. Diff overlays show unmatched viewer edges in red, unmatched exporter edges in cyan, and covered edges in gray.

Cases keep rotation, origin inference, format dispatch, and the physical USB mounting example separate. Explicit-origin zero/Z cases are controls; missing-origin cases retain their original alignment tags. Generated output lives in ignored directories, never committed PNG baselines. See tests/fixtures/renderer-parity/README.md for source provenance and the common-board adaptation.

Nonzero angle probes use oblique values (for example X=37, Y=30, Z=47, and mixed 23/31/47), not quarter turns that can conceal symmetry and axis mistakes. Zero angles and the renderer's implicit bottom-layer fallback remain deliberate controls. The physical USB mounting fixture is a documented exception: its native Z-up mesh requires Z=270 degrees to fit the footprint. Actual tab/hole and contact/pad geometry is checked separately. The original USB missing-origin input remains a distinct diagnostic, not the mounted pose's zero-angle control.

Custom Component Models

You can define custom 3D models for components using the cadModel prop:

<chip
  name="U1"
  footprint="soic8"
  cadModel={{
    objUrl: "/path/to/custom-model.obj",
    mtlUrl: "/path/to/custom-material.mtl",
  }}
/>

JSCAD Models

For more complex or programmatically defined models, you can use JSCAD:

<bug
  footprint="soic8"
  name="U1"
  cadModel={{
    jscad: {
      type: "cube",
      size: 5,
    },
  }}
/>

Contributing

We welcome contributions! Please see our Contributing Guide for more details.

Related Projects

License

This project is licensed under the MIT License - see the LICENSE file for details.