90 خطوط
10 KiB
Markdown
90 خطوط
10 KiB
Markdown
# Schematic Diff Viewer
|
|
|
|
The comparison feature reuses native SchDoc parsing, the original vector geometry, the SVG serializer, the existing PostgreSQL/SQLite durable job queue, bounded parser subprocesses, local/S3 storage, API-key authentication and expiring viewer capabilities. It adds no runtime or frontend packages and no database columns/tables/migration.
|
|
|
|
Open `/compare` to select an original and modified file, or follow the comparison link on the main upload page. After processing, `/compare/{id}#token=…` shows the original on the left and modified document on the right. Original schematic primitives keep their colors and geometry; optional boxes annotate changes. Added objects are green, removed red, modified amber and moved blue. A changes list provides property details, previous/next navigation and click-to-focus. Zoom/pan/pinch, synchronized world coordinates, fit, category filtering, changes-only mode and highlight toggling are available. Both drawings have unique SVG clip identifiers to avoid collisions in the same DOM.
|
|
|
|
The single-file viewer and its fullscreen interface remain unchanged. The new comparison page is a separate workspace. Existing iframe policy remains limited to `/view/*`; `/compare/*` is a standalone page and is not additionally granted cross-origin framing permission by this feature.
|
|
|
|
## API
|
|
|
|
Primary versioned endpoint: `POST /api/v1/comparisons`. `POST /api/compare` is an alias for the same implementation. Upload authentication is the existing `X-API-Key` header.
|
|
|
|
```bash
|
|
curl -X POST http://127.0.0.1:8000/api/compare \
|
|
-H "X-API-Key: $API_KEY" \
|
|
-F 'old_file=@original.SchDoc' \
|
|
-F 'new_file=@modified.SchDoc' \
|
|
-F 'access=private' \
|
|
-F 'expires_in=3600'
|
|
```
|
|
|
|
Response: HTTP **202**, matching the existing asynchronous rendering convention:
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"id": "96a2db531ed846e79be507eec36e00ba4c09a142d67ab35d",
|
|
"comparison_id": "96a2db531ed846e79be507eec36e00ba4c09a142d67ab35d",
|
|
"status": "queued",
|
|
"url": "http://127.0.0.1:8000/compare/96a2db531ed846e79be507eec36e00ba4c09a142d67ab35d#token=READ_TOKEN",
|
|
"viewer_url": "http://127.0.0.1:8000/compare/96a2db531ed846e79be507eec36e00ba4c09a142d67ab35d#token=READ_TOKEN",
|
|
"viewer_token": "READ_TOKEN",
|
|
"status_url": "http://127.0.0.1:8000/api/v1/comparisons/96a2db531ed846e79be507eec36e00ba4c09a142d67ab35d",
|
|
"access": "private",
|
|
"expires_at": 1794040000
|
|
}
|
|
```
|
|
|
|
Retain the initial URL including its token fragment. Opening it immediately is supported: the workspace polls until processing completes. `summary` is available only when the worker finishes; a queued upload cannot truthfully report change counts yet.
|
|
|
|
Poll `GET /api/v1/comparisons/{id}` (alias `GET /api/compare/{id}`) with `X-Viewer-Token: READ_TOKEN` or the existing service API key. The states and JSON error envelope follow the rendering API: queued, processing, completed, failed. A malformed file may pass initial envelope identification and fail later in the native parser. Missing/bad private tokens receive 404; expired authorized links receive 410 until cleanup. `access=unlisted` is an explicit alternative with the same unpredictable identifiers as the single viewer.
|
|
|
|
Completed responses include:
|
|
|
|
- `summary`: mutually exclusive primary counts `added`, `removed`, `modified`, `moved`; their sum equals `total_changed`.
|
|
- `changes`: status, kind, label, old/new source object IDs, old/new bounds and primitive IDs, matching method, property differences and movement delta of the stored anchor in document units. When geometry also changes, an anchor delta does not assert rigid translation of the entire object. An object that changes properties and moves has primary status `modified` plus `moved: true` and `movement`; it is not counted twice.
|
|
- `old` and `new`: filenames, source SHA-256, original sheet/content bounds and parser diagnostics. Initial camera fit uses combined content bounds, while keeping both panes in the same native coordinate system.
|
|
- `nets`: added/removed observed net names, and names whose label/port anchor sets changed. A rename is also a modified label with old/new TEXT/NAME in its field changes.
|
|
- `matching`: counts of paired/unpaired objects and ambiguous remaining signature groups.
|
|
- `limitations`: explicit scope and uncertainty notes.
|
|
|
|
`GET /api/v1/comparisons/{id}/svg/old` and `/svg/new` return annotated native SVGs with the same read permission checks. Pending SVGs return 409; failed SVGs 422. Existing single-schematic endpoints remain unchanged; comparison clients should use the comparison endpoints and initial comparison URL.
|
|
|
|
## Structural matching and interpretation
|
|
|
|
Objects owned by a component are aggregated into one component change rather than counting every pin, symbol segment and designator as independent additions. Component-owned geometry is compared relative to its original placement, so translating a component and its children does not invent internal symbol changes. Properties, parameters, values, designators, rotations, mirrored state, part selection and symbol-record signatures are compared. Embedded raster changes are detected by payload hashes even when filenames match. Unowned wires, labels, ports, junctions, shapes and other supported/source objects are compared separately; sheet properties are compared as sheet settings.
|
|
|
|
Matching stages are: unique native `UNIQUEID` within the object kind, unique component designator/unit, exact structural signature and position, unique translation-independent structure, then unique kind/position. Only keys unique on both sides are paired at each heuristic stage. Identical repeated objects are compared as multisets. Physical FileHeader/Additional record indexes never serve as persistent cross-file identity, and changes in record order do not create changes by themselves. Ownership/index bookkeeping is ignored. Fractional coordinates are preserved. Wire vertex reversal is normalized as the same undirected wire geometry.
|
|
|
|
Fallback matching is necessarily heuristic when stable IDs are absent or regenerated: an object replaced at the same position may look like a modification. Ambiguous objects remain added/removed instead of receiving arbitrary nearest-neighbor matches. Every pair records its matching method. Changes to unsupported objects can be identified from retained properties, but cannot be faithfully highlighted if the parser has no drawable geometry. Such changes remain in the list with no fabricated bounding box.
|
|
|
|
This is **not an Altium compiled netlist comparison**. Wire reroutes, pin positions, net labels and connection anchors can reveal graphic changes, but do not prove an electrical connection was added/removed or changed. Bus expansion, harness routing, repeated channels and cross-sheet/project-level connectivity are not resolved. Do not use these graphics as proof of electrical equivalence. Parser/font/format fidelity limitations from [compatibility](COMPATIBILITY.md) still apply independently to both sides. No pixel-difference engine or generated substitute symbol library is involved.
|
|
|
|
## Queue, storage and deployment
|
|
|
|
A comparison occupies one existing `Job` row and one queue slot. Its application-generated input key is `{id}/comparison.json`, an immutable manifest with the two sanitized filenames; the actual sources are `{id}/old.SchDoc` and `{id}/new.SchDoc`. The worker uses that key type to dispatch `app.compare_task` through the same process, resource limits, retry lease and stale-attempt fencing. Successful attempts write `data.json`, `old.svg` and `new.svg` into the existing attempt-scoped storage prefix. No runtime data is embedded in the frontend. Expiry cleanup removes both sources, the manifest and attempt artifacts. Local and S3 paths use the same strict key validation. Multi-file upload failures clean up already-written inputs before a job is admitted.
|
|
|
|
Comparison parsing explicitly permits valid empty sheets so removing all objects can be compared. The single-file parser retains its existing empty-geometry rejection by default. Each file retains `MAX_UPLOAD_BYTES` (default 32 MiB). Only the two comparison POST paths get a combined multipart limit of `2 * MAX_UPLOAD_BYTES + 1 MiB`; single-file requests keep their original bound. The Caddy example keeps its existing general request limit and adds 65 MiB for the two comparison paths. Adjust external CDN/proxy upload caps correspondingly when deploying larger comparisons. This changes request size handling, not CSP/CORS policy. The same total CPU/memory/wall-clock limits apply per comparison job, so extremely complex pairs may hit resource limits even if each file renders separately.
|
|
|
|
Restart API and workers after updating. With Compose:
|
|
|
|
```bash
|
|
docker compose up -d --build api worker
|
|
```
|
|
|
|
If using the production Caddy overlay, reload/recreate its proxy to pick up the route-specific multipart limit. A schema migration is not needed.
|
|
|
|
## Validation
|
|
|
|
```bash
|
|
SCHDOC_SAMPLE='/path/to/real.SchDoc' ./scripts/check.sh
|
|
API_KEY="$API_KEY" .venv/bin/python scripts/compare-smoke.py original.SchDoc modified.SchDoc
|
|
```
|
|
|
|
Tests cover record reordering, component UID matching/renumbering, values, pure movement, combined movement/property edits, additions/removals, reversed wire vertices, duplicate objects, labels/net-name scope, embedded raster replacement, nonmutation, original SVG geometry, clip-ID separation, upload/authentication/CORS, per-file/combined limits, partial storage failures, a real OLE same-file comparison, private result/SVG access, failed processing and expiry cleanup. Frontend camera adapter tests cover pane offsets, synchronized world cameras, pinch, change focus and detached listeners. Synthetic unit circuits never appear as application data.
|
|
|
|
The actual local `Sheet1-before.SchDoc` / `Sheet1-after.SchDoc` pair (205/206 source records) produced 1 moved component, 2 modified wires and 1 added junction. The 1,942-record CPU sheet compared with itself produced zero changes. These are structural integration checks, not a trusted Altium visual comparison or browser UI acceptance test. No proprietary samples are redistributed. Browser interaction and visual validation require a connected browser; absence of browser QA must be reported rather than replaced by camera unit tests.
|
|
|
|
Implementation verification: 115 Python tests and 6 frontend tests passed with the native fixtures enabled. The Docker image built successfully, the Caddy configuration adapted successfully, and real HTTP comparison plus single-file smoke checks passed against PostgreSQL with two worker replicas. The comparison result and SVG routes enforced private read tokens. A connected browser was unavailable, so interactive workspace appearance and browser behavior are not claimed as verified.
|