@syncpcb/syncpcb-compare
v0.1.2
Published
Compare two PCB files and return differences, change files, and reports
Downloads
173
Maintainers
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-compareAPI
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 reportOptions
| 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 singleModifiedentry 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
nameis excluded fromchangedFields.
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/RejectedisProposalComplete(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/RejectedisResponse(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/RejectedisResponseComplete(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 ProposeddiffToChangeRequest(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