npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@syncpcb/syncpcb-compare

v0.1.2

Published

Compare two PCB files and return differences, change files, and reports

Downloads

173

Readme

@syncpcb/syncpcb-compare

Compare two SyncPCB/IDX files and produce structured diffs, human-readable reports, and WorkspaceData change requests.

Install

npm install @syncpcb/syncpcb-compare

API

compare(a, b, options?)

Compares two SyncPCBData objects and returns a CompareResult.

import { compare } from '@syncpcb/syncpcb-compare';

const result = compare(baseline, revised, { useIds: false });

result.summary   // { added, removed, modified, total }
result.changes   // ChangeRecord[] — one entry per changed element
result.diff      // SyncPCBData containing only changed elements (idxFileType=Change)
result.report    // human-readable text report

Options

| Option | Type | Default | Description | |---|---|---|---| | useIds | boolean | true | Matching strategy — see below. | | tolerance | number | 1e-9 | Numeric tolerance for coordinate comparison. |

Matching strategies (useIds)

useIds: false — recommended for semantic comparison

Uses coordinate-based geometric keys to match elements, so comparisons remain stable even when geometry IDs shift between versions (e.g. when a new curveSet is inserted and all subsequent IDs increment).

  • Keepouts / keepins: two-phase matching. Phase 1 pairs items by resolved shape coordinates — a keepout renamed at the same position (e.g. ROUTE_KEEPOUT_ALL → ROUTE_KEEPOUT_BOTTOM) is matched and not reported as a change. Phase 2 falls back to identifier ID for any items left unmatched after phase 1, so a keepout that moved or changed shape is reported as a single Modified entry rather than a spurious removed+added pair.
  • Board regions: matched by name; shape changes are detected by comparing the resolved coordinate signatures of each shape (not the string IDs), so adding a second shape or changing the board outline is correctly reported as Modified.
  • Name-only renames on keepouts/keepins are not reported since name is excluded from changedFields.

useIds: true — required when the diff will be passed to applyDiff

Matches elements by identifier.id. Keepouts/keepins use direct ID matching with geometric shape comparison, producing ID-stable diffs that applyDiff can apply correctly.


applyDiff(base, diff)

Applies a diff SyncPCBData (produced by compare) back onto a base SyncPCBData.

import { applyDiff } from '@syncpcb/syncpcb-compare';

const updated = applyDiff(baseline, diff);
// updated.header.idxFileType === 'Baseline'

Elements tagged ChangeType.Removed are deleted from the base; all others are upserted. Components merge their instance lists independently. The returned header takes designId from the diff and description from the base.

Change annotations (changeType / changeStatus)

Elements, components and instances present in the diff keep the diff's changeType/changeStatus in the result (e.g. Added/Proposed, or Added/Accepted after a later response diff is applied). Any changeType/changeStatus left over on base elements from a previous apply is stripped, so the result only annotates what changed in this diff — base elements not touched by the diff come through with no changeType/changeStatus at all.

A diff item tagged changeType: Added with a changeStatus (i.e. an EDMD proposal/response item) and an id that already exists in the base updates that element in place — this is how a response diff carries a later changeStatus (e.g. Accepted/Rejected) for an item a previous proposal diff added. Without a changeStatus (a plain compare() diff), an Added item whose id collides with an existing base element is instead appended under a fresh id — see "Geometry renumbering repair" below.

Geometry renumbering repair

When diffs are generated with useIds: false, inserting new geometry elements can shift subsequent IDs (e.g. a new curveSet at IDF_CS_1 pushes the old one to IDF_CS_2). applyDiff automatically remaps keepout/keepin shape references after applying the geometry diff, using coordinate signatures to locate the correct element in the updated geometry. This allows useIds: false diffs to be applied correctly through a sequential chain.

Sequential apply chain

// Apply diffs 1→2, 2→3, … 8→9 in sequence starting from test_idf_1
let current = loadIdf(1);
for (let a = 1; a <= 8; a++) {
  const diff    = compare(loadIdf(a), loadIdf(a + 1), { useIds: false }).diff;
  current       = applyDiff(current, diff);
  // current is now semantically equivalent to loadIdf(a + 1)
}

summariseDiff(diff)

Produces a concise human-readable summary from a diff SyncPCBData.

import { summariseDiff } from '@syncpcb/syncpcb-compare';

const text = summariseDiff(diff);
console.log(text);

filterByChangeStatus(data, status, options?)

Returns a SyncPCBData containing only the board elements (components, keepouts, board regions, etc.) whose changeStatus matches the given status.

import { filterByChangeStatus, ChangeStatus } from '@syncpcb/syncpcb-compare';

const accepted = filterByChangeStatus(applied, ChangeStatus.Accepted);
const responded = filterByChangeStatus(applied, [ChangeStatus.Accepted, ChangeStatus.Rejected]);

Options

| Option | Type | Default | Description | |---|---|---|---| | includeGeometry | boolean | false | Include board.geometry in the output, limited to the elements (and their transitive curveSet/polyLine/arc/circle/line/point dependencies) referenced via shapes by the filtered-in board elements. When false, geometry arrays are returned empty. |


EDMD workflow helpers

getProposal, getResponse, isProposal, isProposalComplete, isResponse and isResponseComplete all classify a SyncPCBData by the changeStatus of its board elements:

| Function | Returns | |---|---| | isProposal(data) | true if any element has changeStatus === Proposed | | isResponse(data) | true if any element has changeStatus of Accepted or Rejected | | isProposalComplete(data) | true if isProposal and not isResponse — a fresh proposal, nothing responded to yet | | isResponseComplete(data) | true if isResponse and not isProposal — every proposed change has a response | | getProposal(data, options?) | The Proposed elements only | | getResponse(data, options?) | The Accepted/Rejected elements only |


getProposal(data, options?) / getResponse(data, options?)

Convenience wrappers around filterByChangeStatus for the EDMD workflow:

import { getProposal, getResponse } from '@syncpcb/syncpcb-compare';

const proposed  = getProposal(applied);                         // changeStatus === Proposed
const responded = getResponse(applied, { includeGeometry: true }); // changeStatus in [Accepted, Rejected]

getProposal is equivalent to filterByChangeStatus(data, ChangeStatus.Proposed, options). getResponse is equivalent to filterByChangeStatus(data, [ChangeStatus.Accepted, ChangeStatus.Rejected], options).


isProposal(data)

Returns true if any board element in data has changeStatus === ChangeStatus.Proposed, i.e. the data contains at least one change still awaiting a response.

import { isProposal } from '@syncpcb/syncpcb-compare';

isProposal(proposal); // true  — newly proposed elements
isProposal(response); // false — every element has been Accepted/Rejected

isProposalComplete(data)

Returns true if isProposal(data) is true and isResponse(data) is false — i.e. there is at least one proposed change and none have been responded to yet.

import { isProposalComplete } from '@syncpcb/syncpcb-compare';

isProposalComplete(proposal); // true  — all elements still Proposed, none responded to
isProposalComplete(partial);  // false — some elements have already been Accepted/Rejected

isResponse(data)

Returns true if any board element in data has changeStatus === ChangeStatus.Accepted or changeStatus === ChangeStatus.Rejected, i.e. the data contains at least one change that has been responded to.

import { isResponse } from '@syncpcb/syncpcb-compare';

isResponse(proposal); // false — no element has been responded to yet
isResponse(response); // true  — elements have been Accepted/Rejected

isResponseComplete(data)

Returns true if isResponse(data) is true and no element remains with changeStatus === ChangeStatus.Proposed — i.e. every proposed change has received a response.

import { isResponseComplete } from '@syncpcb/syncpcb-compare';

isResponseComplete(response); // true  — all elements Accepted/Rejected
isResponseComplete(partial);  // false — some elements still Proposed

diffToChangeRequest(diff, options?)

Converts a diff SyncPCBData into a ChangeRequest as defined in WorkspaceData.

import { diffToChangeRequest } from '@syncpcb/syncpcb-compare/datamodel/diffToChangeRequest';

const cr = diffToChangeRequest(diff, { order: 1, status: 'pending' });

Each component in the diff becomes one Change entry. Geometry changes are grouped into a single summary entry. Board structural changes (layers, keepouts, copper, etc.) are grouped into a second summary entry if present.

Options

| Option | Type | Default | Description | |---|---|---|---| | order | number | 1 | order field on the returned ChangeRequest. | | status | 'pending' \| 'merged' | 'pending' | Initial status of the change request. |


Types

import type {
  CompareOptions,
  CompareResult,
  ChangeRecord,
  ChangeType,
  ChangeStatus,
  CompareSummary,
  FilterByChangeStatusOptions,
} from '@syncpcb/syncpcb-compare';

WorkspaceData types (ChangeRequest, Change, Approval, BoardHistory, etc.) live in src/datamodel/WorkspaceData.ts.


Output files

Sequential test_idf files

src/test_idf/test_idf_1.json through test_idf_9.json are a set of IDF boards that evolve incrementally — each file adds or modifies one feature (keepouts, keepins, board region shapes, components).

| File | Description | |---|---| | src/differences/test_idf_N_vs_M_diff.json | Semantic diff between sequential versions (useIds=false) | | src/applied/test_idf_N_applied.json | Result of applying the sequential diff chain from test_idf_1 |

Named test_idf variants

| File | Description | |---|---| | src/test_idf/test_idf.json | Full reference board | | src/test_idf/test_idf_bo.json | Board outline changed; first shape modified and second shape added to the board region — only the board region differs | | src/test_idf/test_idf_MKO_CKO.json | Two keepouts changed — one moved (circle centre changed) and one reshaped (curveSet changed) | | src/differences/test_idf_vs_bo_diff.json | Diff: test_idf vs test_idf_bo (1 board region modified) | | src/differences/test_idf_vs_MKO_CKO_diff.json | Diff: test_idf vs test_idf_MKO_CKO (2 keepouts modified) |

EDMD accept/reject workflow

src/edmd/ contains a worked example of the EDMD (ECAD-MCAD Design Data) proposal/response workflow: a Baseline board, a Change proposal that adds 3 components (changeStatus: Proposed), and a Response that accepts one and rejects the other two.

| File | Description | |---|---| | src/edmd/baseline_accept_all.json | Baseline board (idxFileType=Baseline) | | src/edmd/proposal_accept_all.json | Proposal adding 3 components U15/U19/U26 (idxFileType=Change, changeType=Added, changeStatus=Proposed) | | src/edmd/response_accept_reject.json | Response to the proposal (idxFileType=Response): U15/U19 Rejected, U26 Accepted | | src/applied/baseline_accept_all_applied.json | applyDiff(baseline, proposal) — all 3 components present with changeStatus=Proposed | | src/applied/baseline_accept_all_applied_proposed.json | filterByChangeStatus(applied, ChangeStatus.Proposed, { includeGeometry: true }) — the 3 proposed components and their referenced geometry | | src/applied/baseline_accept_reject_applied.json | applyDiff(applied, response) — all 3 components present, changeStatus updated to Rejected/Rejected/Accepted | | src/applied/baseline_accept_reject_applied_accepted.json | filterByChangeStatus(applied, ChangeStatus.Accepted) — only U26 | | src/applied/baseline_accept_reject_applied_responded.json | filterByChangeStatus(applied, [ChangeStatus.Accepted, ChangeStatus.Rejected], { includeGeometry: true }) — all 3 with referenced geometry | | src/applied/baseline_accept_reject_applied_responded_nogeom.json | Same filter with includeGeometry: false — all 3, geometry empty | | src/applied/baseline_accept_reject_applied_responded_with_keepout.json | applyDiff(baseline_accept_reject_applied_responded, proposal_add_keepout) — the 3 responded components (changeType/changeStatus stripped, since this diff doesn't touch them) plus the newly proposed keepout and its circle/point geometry |

Two further independent proposal/response pairs, also starting from baseline_accept_all.json, exercise other element types and change types:

| File | Description | |---|---| | src/edmd/proposal_add_keepout.json | Proposal adding a circular Route keepout (idxFileType=Change, changeType=Added, changeStatus=Proposed) plus its circle/point geometry | | src/edmd/response_add_keepout_reject.json | Response (idxFileType=Response): the keepout is Rejected | | src/applied/baseline_add_keepout_applied.json | applyDiff of both in sequence — the keepout remains in the board with changeStatus=Rejected | | src/applied/baseline_add_keepout_applied_rejected.json | filterByChangeStatus(applied, ChangeStatus.Rejected, { includeGeometry: true }) — the keepout with its circle and center point | | src/edmd/proposal_modify_boardregion.json | Proposal modifying the existing board region (idxFileType=Change, changeType=Modified, changeStatus=Proposed) — adds a second (cutout) shape plus its circle/point geometry | | src/edmd/response_modify_boardregion_accept.json | Response (idxFileType=Response): the modification is Accepted | | src/applied/baseline_modify_boardregion_applied.json | applyDiff of both in sequence — the board region keeps both shapes with changeStatus=Accepted | | src/applied/baseline_modify_boardregion_applied_accepted.json | filterByChangeStatus(applied, ChangeStatus.Accepted, { includeGeometry: true }) — the region with its full referenced geometry (original outline + new cutout) |


Development

npm test          # run Jest test suite
npm run build     # compile to dist/
npm run lint      # ESLint