Files
altium-render-engine/docs/PCB.md
T
2026-11-09 12:34:50 +03:30

15 KiB

Native PCB preview and structural comparison

Two new creation endpoints extend the existing FastAPI application. Existing schematic API paths, private capabilities and SVG output remain supported. No database migration, Redis, frontend framework or new runtime dependency is introduced. The original schematics table stores all four job kinds.

API

Method Path Purpose
POST /api/v1/pcbs/render Upload file as .PcbDoc
POST /api/v1/pcbs/comparisons Upload old_file and new_file as .PcbDoc
GET /api/v1/pcbs/{id}/status Render status and completed layer inventory
GET /api/v1/pcbs/{id}/data Completed PCB document
GET /api/v1/pcbs/{id}/svg Completed layer-aware SVG
GET /api/v1/pcbs/comparisons/{id} Status and completed structural report
GET /api/v1/pcbs/comparisons/{id}/svg/{side} Annotated SVG; side is old or new
GET /view/pcb/{id} Fullscreen single PCB shell
GET /compare/pcb/{id} Fullscreen side-by-side PCB shell
curl -X POST https://render.sadentel.net/api/v1/pcbs/render \
  -H "X-API-Key: $API_KEY" \
  -F 'file=@board.PcbDoc' -F 'access=private' -F 'expires_in=3600'

curl -X POST https://render.sadentel.net/api/v1/pcbs/comparisons \
  -H "X-API-Key: $API_KEY" \
  -F 'old_file=@original.PcbDoc' -F 'new_file=@modified.PcbDoc' \
  -F 'access=private' -F 'expires_in=3600'

Both creation requests return HTTP 202, Location: status_url and Retry-After: 1. access defaults to private; unlisted is supported. Expiry is measured from admission, including queue time, and cannot exceed LINK_TTL_SECONDS.

Example render response (identifiers, timestamps and capabilities are illustrative):

{
  "success": true,
  "id": "0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "queued",
  "document_type": "pcb",
  "viewer_url": "https://render.sadentel.net/view/pcb/0123456789abcdef0123456789abcdef0123456789abcdef#token=READ_TOKEN",
  "status_url": "https://render.sadentel.net/api/v1/pcbs/0123456789abcdef0123456789abcdef0123456789abcdef/status",
  "viewer_token": "READ_TOKEN",
  "access": "private",
  "expires_at": 1794818000
}

Comparison responses use document_type: pcb_comparison and add comparison_id and url, equal to the initial viewer_url. The viewer path is /compare/pcb/{id} and status path is /api/v1/pcbs/comparisons/{id}. Queued responses do not invent a summary. Completed reports contain summary (added/removed/modified/moved), total_changed, changes, old, new, nets, matching and limitations.

Retain the initial private URL and token. Status responses never return the capability. Reads accept X-Viewer-Token or the service X-API-Key. Unauthorized private reads return 404; authorized expired reads return 410 until cleanup. Pending artifacts return 409; failed artifacts return 422. Failed jobs retain their error in an HTTP 200 status response. PCB validation/decoding errors use invalid_pcb; resource-limit and infrastructure errors use the existing queue conventions. Filenames and geometry are not disclosed by unauthenticated status requests.

Viewer shells themselves are public static shells, like the schematic viewer; drawing data is fetched with the capability header. Token fragments are removed from browser history and stored in a resource-specific sessionStorage key. Browser storage restrictions in embedded contexts still require browser acceptance testing. No API key is shipped to the browser.

Queue and storage compatibility

Immutable application-owned input names classify jobs:

Input name Job kind
input.SchDoc schematic_render (legacy)
comparison.json schematic_comparison (legacy)
input.PcbDoc pcb_render
pcb-comparison.json pcb_comparison

A PCB comparison stores two sources old.PcbDoc and new.PcbDoc, plus a versioned manifest. The worker dispatches PCB work to app.pcb_task, inheriting the existing subprocess CPU, address-space, output, FD and wall-clock limits. Attempts write the same attempt-{1|2}/data.json, drawing.svg or old.svg/new.svg artifact names. Completion uses the existing attempt/status fence. API routes require the exact resource kind even for unlisted jobs and service-key reads. The storage regex permits only these named additions. Cleanup removes both source files and all attempts.

Each input remains bounded by MAX_UPLOAD_BYTES. Only the new comparison POST joins the existing two-file multipart allowance (2 * MAX_UPLOAD_BYTES + 1 MiB). The Caddy comparison matcher includes that exact new path with 65 MiB; other request limits remain unchanged. Adjust external CDN caps if required by your deployment.

PCB filenames are sanitized, MIME is not trusted, and the API checks the actual OLE header/Board6 identification with bounded stream reads. Full binary decoding remains asynchronous. Renaming PCB bytes to .SchDoc does not admit them to the schematic API; SchDoc bytes are rejected by PCB validation.

Native format and geometry

pcb_binary.py, pcb_geometry.py and pcb_parser.py adapt an independently written reader from the user's local Altium viewer checkout; no other application/frontend/service is imported. The binary layouts and units were cross-checked with the KiCad developer format reference. No GPL importer implementation is copied, and no Altium SDK is required.

Supported envelope: PCB 6.0 Binary File in FileHeaderSix, with Board6/Data identifying Protel_Advanced_PCB. Board format revisions 5.0, 5.01 and 6.0 are recognized; only 5.01 is verified on the local native files. These are format revisions, not supported Altium application release promises. Other envelopes/revisions fail explicitly.

Readers validate framing, block sizes, field boundaries, finite numbers, references, stream counts and cumulative stream sizes. They never resynchronize after a bad tag, execute embedded resources or fetch external URLs. Limits include 1024 streams, 64 MiB cumulative stream bytes, 16 MiB subrecords and 250,000 primitives, in addition to subprocess limits. Very large boards may exceed these limits.

Binary coordinates are integer 1/10000 mil. The shared world unit is 10 mil:

world_x = raw_x / 100000
world_y = -raw_y / 100000

Placed child geometry is already global and is not rotated twice. PCB-specific models retain footprint metadata, object provenance, optional native UID, layer memberships and drill spans, without turning PCB pads into schematic pins. Source physical indexes remain provenance only.

Native streams Current treatment
Board6 Real boundary vertices, arcs, dimensions and stack-link metadata
Components6 / Nets6 Footprint placement, parameters and actual net references
Tracks6 / Arcs6 Centerline geometry, widths and layers
Pads6 Top/mid/bottom and supported full-stack shapes, rotation, plated state, round/square/slot drill and supported offsets
Vias6 Recorded span, primary and supported per-layer diameters, drill
Fills6 Rotated filled rectangles
Regions6 Saved polygon copper with interior holes
ShapeBasedRegions6 Standalone solid/curved regions and supported board cutouts; duplicate polygon definitions are not painted twice
Polygons6 Retained definition boundaries; never treated as freshly poured copper
Texts6 / WideStrings6 Stored strings and placement; font/style limitations are diagnosed
UniqueIDPrimitiveInformation Native primitive identity where present

Copper membership follows actual Board6 links, including plane-layer identities. Pads and vias keep multiple layer memberships; per-layer geometry is drawn separately. Drill visibility follows its recorded span. The safe SVG serializer emits layer identifiers and inert geometry, with top-side visibility initially selected. Layer controls allow Top/Bottom/All and individual selection. Outline is a separate layer. Changing the preset does not geometrically mirror or rearrange the board.

Fidelity boundaries

Compatibility is explicitly partial:

  • Layer colors are a viewer palette; Altium application display preferences are not claimed to be embedded or reproduced.
  • Browser fonts replace native stroke/TrueType rendering; metrics, barcodes, inverted text and special styles may differ. Font licensing/availability still applies.
  • Saved pours are rendered as stored, possibly stale. There is no repour engine, rule execution, negative-plane reconstruction or solder/paste-mask rule expansion.
  • Supported board cutouts are rendered as visual cutouts; this is not a fabrication geometry boolean engine.
  • Advanced/unknown pad shapes are omitted with diagnostics. Undecoded full-stack pad or nonuniform via dimensions on internal plane layers are reported rather than assigned an invented shape.
  • Embedded STEP models, 3D bodies, rules and unsupported streams are not rendered. Diagnostics identify omitted Data streams and declared counts when present.
  • Footprints without supported native geometry remain in metadata with diagnostics; substitute symbols are never generated.
  • Geometry outside the board bounds remains pannable but may start outside the initial viewBox.

No proprietary sample, Altium software, SDK, font or compiled library is included in the repository or Docker image. No pixel-perfect claim is made.

Diff interpretation

The PCB diff reuses the format-independent entity matcher, not schematic record-number semantics or schematic coordinate-key normalization. It matches unique native IDs, unique footprint designators, exact geometry/position, unique local structure, then unique same-kind position. Ambiguous objects remain additions/removals; identical duplicates are treated as multisets. Each match records its method.

Footprint-owned geometry is aggregated and compared in local coordinates, while independent tracks/vias remain independent. Layer, net name, width, rotation, drill, rendered geometry and properties participate. Net indexes and record/component/polygon indexes are excluded as cross-file identities. Reversed straight tracks and net renumbering do not create changes by themselves. The stack is a separate semantic entity. Unsupported stream bytes are not semantically compared, and this limitation is part of every report.

Primary added/removed/modified/moved counts are disjoint. A modified-and-moved object has primary modified plus moved: true and a movement delta. Rotation changes are reported in fields and rotation_change. An anchor delta is not proof of rigid translation when geometry also changed.

Net results report observed names added/removed; object fields reveal changed stored assignments. The result is not DRC, ERC, an electrical-equivalence proof, a manufacturing comparison or a project-wide compiled netlist.

Viewer and framing

The dependency-free PCB shell uses the existing camera/interactions module and spinner. The default is a fullscreen board or two boards, plus a menu button. Layers, diagnostics and comparison controls appear only when the menu opens; there is no new dashboard or upload page. Comparison supports synchronized pan/zoom/pinch, fit, change filters, changes-only, highlights and previous/next focus.

Existing security middleware covers /view/* and /compare/*, including the new nested PCB pages. Those responses have one CSP with frame-ancestors 'self' https://git.sadentel.net; conflicting X-Frame-Options is removed. Other directives and API policies stay intact. Parent-site CSP and any deployed CDN overrides still need environment-specific checking.

Validation and deployment

PCBDOC_SAMPLE='/path/to/private-board.PcbDoc' ./scripts/check.sh
API_KEY="$API_KEY" .venv/bin/python scripts/pcb-smoke.py original.PcbDoc modified.PcbDoc \
  --url http://127.0.0.1:8000

Without a configured native file the real-PCB test is explicitly skipped. Synthetic native unit fixtures are test-only, never fallback viewer content. The HTTP smoke creates private resources, polls the real worker, checks SVGs, denies unauthenticated reads, and checks preview security headers without printing capabilities. Omit modified.PcbDoc to verify a same-file comparison has zero changes.

Rebuild/restart both API and worker:

docker compose up -d --build api worker

Reload/recreate Caddy if using the production overlay so its two-file upload matcher is updated. No schema migration is required. No automatic production deployment or unrelated service restart is performed by this feature.

Local verification evidence (2026-11-09)

The current /home/nearology/LTE 6/LTE 6.PcbDoc (SHA-256 2afb5b464cff6c4b780386bf14f39c810230d5625d7277ff01fe4823999ea561) decoded 694 footprints, 479 nets, 12,916 tracks, 311 arcs, 2,329 pads, 1,059 vias, 1,822 texts, 13 fills, 45 polygon definitions and 48 records in each region stream. It produced 26,471 layer-aware primitives. A self-comparison reported zero changes. Its native SVG was rasterized locally and the top-side image inspected; this is not an Altium-reference visual regression.

The other local PCB file (SHA-256 00b812958357b12f62f8d3cca12486f716925b2655d35258115b01c44eaeb314) decoded 729 footprints and produced 41,285 primitives. Comparing those two real local documents completed with 8,073 added, 12,248 removed, 1,300 modified and 469 moved primary entities; 102 ambiguous signature groups remained. These counts demonstrate a completed structural comparison, not human verification of every pair or design equivalence. No claim is made about the chronological relationship of these files.

Real HTTP smoke checks passed against an isolated loopback Uvicorn API with SQLite and a separate real worker: private single rendering, self-comparison, two-file comparison, both SVG reads, unauthorized reads and preview headers. The test processes and their temporary database/storage were stopped/removed afterwards. This run is not evidence of live PostgreSQL/S3 integration or production deployment. S3 coverage uses botocore protocol stubs. Caddy configuration adaptation passed with 65 MiB on the three comparison upload paths and the original 33 MB elsewhere.

No connected browser was available, so actual pan/pinch/menu/iframe appearance remains unverified in a browser. No trusted Altium export was supplied, so geometric and font fidelity has not been certified against Altium output.

The final automated run includes native SchDoc and PCB fixtures, 154 passing Python tests and three passing frontend test files (camera, pointer interaction adapter and PCB layer policies). The standard Docker build was attempted with both normal and host networking, but dependency download stalled on Temporary failure in name resolution for PyPI's package host; those build attempts were stopped. A new Docker image was therefore not validated or deployed in this environment. The existing Dockerfile and dependency lock remain unchanged.