@dejadex/web 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +222 -2
  3. package/dist/errors.d.ts +8 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +9 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/geometry/homography.d.ts +75 -0
  8. package/dist/geometry/homography.d.ts.map +1 -0
  9. package/dist/geometry/homography.js +146 -0
  10. package/dist/geometry/homography.js.map +1 -0
  11. package/dist/identify/assets.d.ts +39 -0
  12. package/dist/identify/assets.d.ts.map +1 -0
  13. package/dist/identify/assets.js +167 -0
  14. package/dist/identify/assets.js.map +1 -0
  15. package/dist/identify/decode.d.ts +6 -0
  16. package/dist/identify/decode.d.ts.map +1 -0
  17. package/dist/identify/decode.js +53 -0
  18. package/dist/identify/decode.js.map +1 -0
  19. package/dist/identify/engine.d.ts +32 -0
  20. package/dist/identify/engine.d.ts.map +1 -0
  21. package/dist/identify/engine.js +127 -0
  22. package/dist/identify/engine.js.map +1 -0
  23. package/dist/identify/identifier.d.ts +91 -0
  24. package/dist/identify/identifier.d.ts.map +1 -0
  25. package/dist/identify/identifier.js +332 -0
  26. package/dist/identify/identifier.js.map +1 -0
  27. package/dist/identify/manifest.d.ts +60 -0
  28. package/dist/identify/manifest.d.ts.map +1 -0
  29. package/dist/identify/manifest.js +211 -0
  30. package/dist/identify/manifest.js.map +1 -0
  31. package/dist/identify/preprocess.d.ts +52 -0
  32. package/dist/identify/preprocess.d.ts.map +1 -0
  33. package/dist/identify/preprocess.js +195 -0
  34. package/dist/identify/preprocess.js.map +1 -0
  35. package/dist/identify/router.d.ts +60 -0
  36. package/dist/identify/router.d.ts.map +1 -0
  37. package/dist/identify/router.js +83 -0
  38. package/dist/identify/router.js.map +1 -0
  39. package/dist/identify/runtime.d.ts +69 -0
  40. package/dist/identify/runtime.d.ts.map +1 -0
  41. package/dist/identify/runtime.js +140 -0
  42. package/dist/identify/runtime.js.map +1 -0
  43. package/dist/identify/search.d.ts +35 -0
  44. package/dist/identify/search.d.ts.map +1 -0
  45. package/dist/identify/search.js +99 -0
  46. package/dist/identify/search.js.map +1 -0
  47. package/dist/identify/server.d.ts +60 -0
  48. package/dist/identify/server.d.ts.map +1 -0
  49. package/dist/identify/server.js +149 -0
  50. package/dist/identify/server.js.map +1 -0
  51. package/dist/identify/shard.d.ts +31 -0
  52. package/dist/identify/shard.d.ts.map +1 -0
  53. package/dist/identify/shard.js +146 -0
  54. package/dist/identify/shard.js.map +1 -0
  55. package/dist/identify/types.d.ts +107 -0
  56. package/dist/identify/types.d.ts.map +1 -0
  57. package/dist/identify/types.js +7 -0
  58. package/dist/identify/types.js.map +1 -0
  59. package/dist/identify/worker.d.ts +2 -0
  60. package/dist/identify/worker.d.ts.map +1 -0
  61. package/dist/identify/worker.js +13 -0
  62. package/dist/identify/worker.js.map +1 -0
  63. package/dist/identify/workerProtocol.d.ts +54 -0
  64. package/dist/identify/workerProtocol.d.ts.map +1 -0
  65. package/dist/identify/workerProtocol.js +113 -0
  66. package/dist/identify/workerProtocol.js.map +1 -0
  67. package/dist/index.d.ts +20 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +16 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/lease/verify.d.ts +69 -0
  72. package/dist/lease/verify.d.ts.map +1 -0
  73. package/dist/lease/verify.js +235 -0
  74. package/dist/lease/verify.js.map +1 -0
  75. package/dist/scanner.d.ts +10 -0
  76. package/dist/scanner.d.ts.map +1 -0
  77. package/dist/scanner.js +58 -0
  78. package/dist/scanner.js.map +1 -0
  79. package/dist/types.d.ts +180 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/usage/meter.d.ts +83 -0
  84. package/dist/usage/meter.d.ts.map +1 -0
  85. package/dist/usage/meter.js +111 -0
  86. package/dist/usage/meter.js.map +1 -0
  87. package/dist/usage/store.d.ts +40 -0
  88. package/dist/usage/store.d.ts.map +1 -0
  89. package/dist/usage/store.js +248 -0
  90. package/dist/usage/store.js.map +1 -0
  91. package/package.json +64 -4
  92. package/src/errors.ts +24 -0
  93. package/src/geometry/homography.ts +178 -0
  94. package/src/identify/assets.ts +189 -0
  95. package/src/identify/decode.ts +54 -0
  96. package/src/identify/engine.ts +167 -0
  97. package/src/identify/identifier.ts +431 -0
  98. package/src/identify/manifest.ts +240 -0
  99. package/src/identify/preprocess.ts +227 -0
  100. package/src/identify/router.ts +115 -0
  101. package/src/identify/runtime.ts +197 -0
  102. package/src/identify/search.ts +115 -0
  103. package/src/identify/server.ts +167 -0
  104. package/src/identify/shard.ts +154 -0
  105. package/src/identify/types.ts +122 -0
  106. package/src/identify/worker.ts +13 -0
  107. package/src/identify/workerProtocol.ts +136 -0
  108. package/src/index.ts +71 -0
  109. package/src/lease/verify.ts +252 -0
  110. package/src/scanner.ts +56 -0
  111. package/src/types.ts +196 -0
  112. package/src/usage/meter.ts +165 -0
  113. package/src/usage/store.ts +282 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@dejadex/web`. Versions follow semver; before 1.0 a
4
+ minor version may break the client contract (`src/types.ts`), and says so here.
5
+
6
+ ## 0.1.1
7
+
8
+ - **Fix:** the usage database (`dejadex-usage`) no longer blocks a host's
9
+ `indexedDB.deleteDatabase`. The IndexedDB store closes its connection on
10
+ `versionchange`; before, a host deleting it on sign-out stayed blocked for
11
+ the life of the page, and the previous account's queued usage could be
12
+ drained under the next account's session.
13
+ - `UsageStore.close()` (optional for stores a host writes; a no-op on the
14
+ memory store). After a close or a `versionchange`, every call rejects with
15
+ `ScannerError` `INVALID_OPTIONS` ("the usage store is closed"); nothing is
16
+ dropped in silence.
17
+ - `destroy()` also closes the usage store the identifier opened itself. A store
18
+ passed as `options.usageStore` belongs to the host and is left open.
19
+
20
+ ## 0.1.0 — first published release
21
+
22
+ - `createIdentifier`: card crops in, catalogue ids out, on the device
23
+ (onnxruntime-web: WebGPU, else WASM SIMD, threaded when the page is
24
+ cross-origin isolated) or through `POST /v1/identify` with the lease.
25
+ - Leases verified on the device with pinned Ed25519 keys (`leasePublicKeys`).
26
+ - Model and index downloads carry the lease's signature: the manifest URL's
27
+ Cloud CDN prefix signature (`URLPrefix`, `Expires`, `KeyName`, `Signature`)
28
+ is copied onto every file URL under that prefix, and onto no other.
29
+ - Usage counted on the device per distinct card per scan session, queued in
30
+ IndexedDB and handed to the host by `drainUsage`.
31
+ - **Contract:** usage is `UsageReport` (`reportId`, `leaseId`, `seq`,
32
+ `sessionId`, `distinctCards`, `cardIds?`, `calls?`, `at?`) in
33
+ `UsageBatch.reports`; it replaced the earlier per-identification
34
+ `UsageEvent`, before any release.
35
+ - `createScanner` and the live pipeline keep their final shape but are still stubs.
package/README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Temporary Holding Version
1
+ # @dejadex/web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Framework-free browser SDK for trading-card identification. Real today:
4
+
5
+ - **`createIdentifier`**: card crops in, catalogue ids out. Identification runs
6
+ on the device (onnxruntime-web: WebGPU, else WASM with SIMD, threaded when the
7
+ page is cross-origin isolated) or through `POST /v1/identify`. The answer has
8
+ the same shape either way. Leases are verified on the device. Usage is counted
9
+ on the device, one per distinct card per session.
10
+ - **The homography maths** (`geometry/`).
11
+
12
+ Still a stub, with its final shape: `createScanner`, the live pipeline
13
+ (detection, tracking, AR). See `openspec/changes/web-sdk-pipeline`.
14
+
15
+ ESM only. Published on the public npm registry, so it installs with no token:
16
+
17
+ ```
18
+ npm install @dejadex/web
19
+ ```
20
+
21
+ What protects the platform is the lease and the signed asset URLs, not the
22
+ SDK's code, which runs in browsers anyway. Releases are cut from a
23
+ `web-v<version>` tag by `.github/workflows/publish-web-sdk.yml` (npm trusted
24
+ publishing, no stored token), staged and then approved by a maintainer with
25
+ 2FA; see `CHANGELOG.md`.
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { createIdentifier } from '@dejadex/web'
31
+
32
+ const identifier = createIdentifier({
33
+ domain: 'tcg01_en',
34
+ // The service's lease-signing keys (its JWKS), pinned in the host's build.
35
+ leasePublicKeys: [{ kty: 'OKP', crv: 'Ed25519', x: '…', kid: '…' }],
36
+ // The identify route's base URL; omit for device-only.
37
+ serverUrl: 'https://api.dejadex.com',
38
+ identification: 'auto', // 'local' never sends an image; 'server' prefers the server
39
+ })
40
+
41
+ // The host's backend mints the lease (POST /v1/leases with its secret key)
42
+ // and passes { token, manifestUrl } through unchanged.
43
+ await identifier.setLease(await myBackend.getLease())
44
+
45
+ const outcome = await identifier.identify(crops /* ≤ 9 JPEG Blobs, one card each */, {
46
+ session: scanSessionId, // one per visit to the scanner; usage counts each card once in it
47
+ purpose: 'live', // 'live' | 'still' | 'retry' (retries are never charged)
48
+ top: 5,
49
+ signal,
50
+ })
51
+ if (outcome.ok) {
52
+ outcome.modelVersion // the model the thresholds belong to
53
+ for (const r of outcome.results) {
54
+ r.index // the crop's position
55
+ r.kind // 'card' | 'not_a_card'
56
+ r.confident // top1 − top2 ≥ the manifest's confidentMargin; never for not_a_card
57
+ r.candidates // [{ cardId, score }], best first
58
+ }
59
+ } else {
60
+ outcome.reason // 'unavailable' | 'timeout' | 'rejected'
61
+ outcome.detail // starts with a stable code where there is one: LEASE_EXPIRED, ALLOWANCE_EXHAUSTED, …
62
+ }
63
+
64
+ // While online, forward usage through the host's backend (it calls POST /v1/usage).
65
+ // A batch leaves the IndexedDB queue only when `deliver` resolves.
66
+ await identifier.drainUsage((batch) => myBackend.postUsage(batch))
67
+ ```
68
+
69
+ `identify` never throws. Other members: `prepare()` downloads and warms the
70
+ device engine and resolves its `DeviceEngineInfo` (backend, precision, threads,
71
+ measured `perCropMs`). `status()` reports the lease, the device engine and why
72
+ the server route is paused. `leaseRequest()` is the renewal body (see Usage).
73
+ `newSession()` and `pendingUsage()` are what their names say. `destroy()`
74
+ releases the device engine and closes the usage store the identifier opened;
75
+ a store passed as `usageStore` is the host's, and stays open.
76
+
77
+ **Sign-out.** Drain usage, `destroy()` the identifier, then
78
+ `indexedDB.deleteDatabase('dejadex-usage')`. An SDK store still open lets go
79
+ of the database when the delete arrives (and rejects later calls), so the
80
+ delete is never blocked and no queued usage carries into the next account.
81
+
82
+ ### Where a call runs
83
+
84
+ | `identification` | Device fast (WebGPU, or ≤ `slowDeviceBudgetMs` = 300 ms per crop) | Device slow, or model still downloading |
85
+ |---|---|---|
86
+ | `auto` | device | server, then device |
87
+ | `server` | server, then device | server, then device |
88
+ | `local` | device | device (waits for the model) |
89
+
90
+ The server is skipped, going straight to the device:
91
+
92
+ - while `navigator.onLine` is false;
93
+ - for the rest of a lease the server refused (401/403/409);
94
+ - until a 429's `Retry-After`;
95
+ - for 30 s after three failures in a row.
96
+
97
+ A server attempt waits at most the device's own time for the same crops, kept
98
+ between 300 ms and 3 s. While the device engine is still loading, it waits up
99
+ to `serverTimeoutMs` (8 s). After that the same crops are identified on the
100
+ device, and a late answer is ignored. A server answer from another model
101
+ version than the lease's is not used.
102
+
103
+ ### What the device engine does
104
+
105
+ It reads the manifest from the lease's `manifestUrl`, whose query is a Cloud CDN
106
+ prefix signature over the lease's model/index directory. The engine copies that
107
+ query onto each file the manifest lists under the signed prefix, on the same
108
+ origin, and onto no other URL. The manifest must match
109
+ the lease's model version, index version and shard. The engine then downloads
110
+ the index shard, its ids and the model file for this device: fp16 on WebGPU
111
+ with `shader-f16`, fp32 on WebGPU without it, and on WASM `runtime.wasmPrecision`,
112
+ fp16 by default. Every file is checked against its SHA-256 and size and cached
113
+ by digest in the Cache API (`dejadex-assets-v1`). Each crop is then:
114
+
115
+ 1. decoded without colour management;
116
+ 2. resized to 224 × 308 with a bit-exact port of Pillow's bilinear filter (the
117
+ gallery's preprocessing), and normalised with ImageNet mean and std;
118
+ 3. embedded at 0° and 180°, taking the L2-normalised CLS token;
119
+ 4. scored against every row by brute-force cosine, keeping the better of the
120
+ two orientations.
121
+
122
+ `kind` and `confident` come from the manifest's `notACardBelow` and
123
+ `confidentMargin`.
124
+
125
+ By default the engine runs in a dedicated module Worker:
126
+ `new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })`.
127
+ Vite, webpack 5 and Rollup bundle that pattern. With Vite, exclude the package
128
+ from pre-bundling (`optimizeDeps.exclude: ['@dejadex/web', 'onnxruntime-web']`),
129
+ or onnxruntime-web loses track of its WASM files. To build the Worker yourself,
130
+ point it at `@dejadex/web/identify-worker` and pass `engine: () => worker`.
131
+ `engine: 'inline'` runs the engine on the calling thread.
132
+
133
+ `runtime.wasmPaths` tells onnxruntime-web where its `.wasm` and `.mjs` files
134
+ are served, e.g. the dev server's `/ort/`.
135
+
136
+ ### Leases
137
+
138
+ `verifyLease(token, { publicKeys, origin, domains })` mirrors the service's
139
+ `service/src/leases/jws.ts`. It accepts an Ed25519 compact JWS with header
140
+ `{ alg: 'EdDSA', typ: 'dejadex-lease+jwt', kid? }` and refuses everything
141
+ else, `alg: none` included. It checks, in order, the signature, the claims'
142
+ shape and 24 h lifetime, expiry (with 60 s of clock skew for `iat`), the page
143
+ origin and the shard. Failures are `LeaseVerificationError`s, which are
144
+ `ScannerError`s whose `reason` is the service's own code: `LEASE_BAD_SIGNATURE`,
145
+ `LEASE_EXPIRED`, `LEASE_ORIGIN_NOT_ALLOWED`, `LEASE_DOMAIN_NOT_ALLOWED`, ….
146
+
147
+ Signatures are checked with WebCrypto Ed25519 where the browser has it
148
+ (Chrome 137+, Safari 17+, Firefox 129+). Elsewhere they fall back to
149
+ `@noble/ed25519` 3.2.0, which is MIT-licensed, audited and has no
150
+ dependencies. It is loaded only when needed.
151
+
152
+ ### Usage
153
+
154
+ Charged: a `kind: 'card'`, `confident` answer whose top `cardId` is new to the
155
+ session. Free: the same card again in the session, `not_a_card`, unsure
156
+ answers and `purpose: 'retry'`. An answer carrying an `identificationId` (the
157
+ service charged it itself) is left out of the report, but still counts toward
158
+ the lease's `allowance`. Once the allowance is used up, `identify` answers
159
+ `rejected` with `ALLOWANCE_EXHAUSTED`.
160
+
161
+ Each call is tallied per (lease, session) in IndexedDB (`dejadex-usage`) before
162
+ it returns. `drainUsage` turns the tallies into `UsageReport`s
163
+ (`{ reportId, leaseId, seq, sessionId, distinctCards, cardIds, calls, at }`,
164
+ with `seq` running per lease from 1). It hands them over in batches of at most
165
+ 500 as `{ deviceId, reports }`, and removes a batch only when `deliver` resolves.
166
+
167
+ Every lease gets at least one report, an empty one if nothing was identified
168
+ under it, because the service renews a lease only once the previous one has
169
+ reported. To renew, drain first, then send `await identifier.leaseRequest()`
170
+ (`{ previousLeaseId, usageReportedThrough }`) to the host's backend.
171
+ `leaseRequest(id)` names a lease the host stored but has not set (it expired,
172
+ or this page never identified); `leaseRequest(null)` asks as a new device.
173
+
174
+ ### Host requirements
175
+
176
+ - **CSP:** `script-src 'wasm-unsafe-eval'`, a `worker-src` for the SDK's
177
+ Worker, and a `connect-src` for the model CDN and the identify route.
178
+ - **Cross-origin isolation:** COOP `same-origin` plus COEP, needed for WASM
179
+ threads. Without it, WASM runs on one thread, which is several times slower.
180
+ Every cross-origin file must then carry CORP or CORS; the dev server sends both.
181
+ - **Privacy notice:** "The camera feed never leaves the device; on devices that
182
+ need it, the cropped image of a single card is sent for identification and
183
+ is not stored."
184
+
185
+ ## Development
186
+
187
+ ```
188
+ npm run build # tsc → dist/
189
+ npm test # vitest
190
+ npm run lint && npm run typecheck
191
+ npm run assets:dev # model + shard + ORT + dev leases on http://127.0.0.1:5350
192
+ npm run measure:identify # time identification on this machine (Node + headless Chromium)
193
+ ```
194
+
195
+ `assets:dev` serves the folder written by `ml/scripts/phase0_export_shard.py`
196
+ (default `ml/artifacts/phase0/shard`). Run that script first. The server provides:
197
+
198
+ - `GET /manifests/<model>/<index>/manifest.json`, plus the shard, ids and `.onnx`
199
+ files it lists, **only with a valid prefix signature**, checked as Cloud CDN
200
+ checks it: an unsigned, expired or wrongly signed request, or one outside the
201
+ lease's pair directory, gets 403 `SIGNATURE_REFUSED`. A URL that lost its
202
+ signature fails here as it would in production;
203
+ - `GET /ort/<file>`, onnxruntime-web's dist, for `runtime.wasmPaths`;
204
+ - `POST /dev/lease { origin }`, which answers `{ token, manifestUrl }`: a lease
205
+ signed with a development key, kept in `.secrets/dev-lease-ed25519.pem`
206
+ (gitignored), and the manifest URL prefix-signed until the lease's `exp`;
207
+ - `GET /dev/lease-key`, the development public key as a JWKS, which a host pins
208
+ in its development configuration.
209
+
210
+ The URL key is the development service's (`service/.secrets/manifest-url-dev.key`,
211
+ name `dev-manifest-key`, from `npm run dev:setup` there; created on first
212
+ start if missing, never printed), so leases from the development service with
213
+ `MODEL_CDN_BASE_URL=http://127.0.0.1:5350` load here as well. `--url-key` and
214
+ `--url-key-name` point elsewhere.
215
+
216
+ Every response carries CORS and `Cross-Origin-Resource-Policy: cross-origin`.
217
+ Private Network Access preflights are answered. To reach it from a phone, use
218
+ `adb reverse tcp:5350 tcp:5350`.
219
+
220
+ The preprocessing parity fixtures in `src/identify/__fixtures__/` come from
221
+ `ml/scripts/phase0_sdk_fixtures.py`: synthetic crops, never card images. The
222
+ embedding half of that test runs only where the gitignored model files are on
223
+ disk. `DEJADEX_REQUIRE_MODELS=1` turns their absence into a failure.
@@ -0,0 +1,8 @@
1
+ /** Machine-readable reasons the scanner refuses or stops. Stable; hosts branch on them. */
2
+ export type ScannerErrorCode = 'INVALID_OPTIONS' | 'NOT_IMPLEMENTED' | 'LEASE_INVALID' | 'LEASE_EXPIRED' | 'ALLOWANCE_EXHAUSTED' | 'ORIGIN_NOT_ALLOWED' | 'DOMAIN_NOT_ALLOWED' | 'UNSUPPORTED_DEVICE' | 'MODEL_INTEGRITY' | 'NETWORK';
3
+ export declare class ScannerError extends Error {
4
+ readonly code: ScannerErrorCode;
5
+ readonly name = "ScannerError";
6
+ constructor(code: ScannerErrorCode, message: string, options?: ErrorOptions);
7
+ }
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,eAAe,GACf,qBAAqB,GACrB,oBAAoB,GACpB,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,SAAS,CAAA;AAEb,qBAAa,YAAa,SAAQ,KAAK;IAInC,QAAQ,CAAC,IAAI,EAAE,gBAAgB;IAHjC,SAAkB,IAAI,kBAAiB;gBAG5B,IAAI,EAAE,gBAAgB,EAC/B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAIzB"}
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ export class ScannerError extends Error {
2
+ code;
3
+ name = 'ScannerError';
4
+ constructor(code, message, options) {
5
+ super(message, options);
6
+ this.code = code;
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAaA,MAAM,OAAO,YAAa,SAAQ,KAAK;IAI1B;IAHO,IAAI,GAAG,cAAc,CAAA;IAEvC,YACW,IAAsB,EAC/B,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAJd,SAAI,GAAJ,IAAI,CAAkB;IAKjC,CAAC;CACF"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Planar homographies for AR labels: the projective map from a card's own
3
+ * unit rectangle to the four corners the detector found on screen, and its
4
+ * CSS `matrix3d` form, so a host page can lay a DOM element on the card's
5
+ * plane with nothing but `transform`.
6
+ *
7
+ * Conventions, fixed for every consumer:
8
+ *
9
+ * - Card space is the unit square: (0,0) top-left, (1,0) top-right, (1,1)
10
+ * bottom-right, (0,1) bottom-left of the card as printed (portrait).
11
+ * - Screen space is CSS pixels of the element the corners were measured in,
12
+ * y pointing down.
13
+ * - A `Quad` lists the screen corners in that same order: TL, TR, BR, BL.
14
+ * - A `Homography` is a 3×3 matrix in ROW-MAJOR order, normalised so the
15
+ * bottom-right entry is 1. It maps (u, v, 1) to (x·w, y·w, w).
16
+ *
17
+ * The square-to-quad solution is the closed form from P. Heckbert,
18
+ * "Fundamentals of Texture Mapping and Image Warping" (1989), §2.2.3: no
19
+ * linear solve, exact for any convex quad, and cheap enough to run per track
20
+ * per frame.
21
+ */
22
+ export interface Point {
23
+ readonly x: number;
24
+ readonly y: number;
25
+ }
26
+ /** Screen corners in card order: top-left, top-right, bottom-right, bottom-left. */
27
+ export type Quad = readonly [Point, Point, Point, Point];
28
+ /** Row-major 3×3 matrix, `[a, b, c, d, e, f, g, h, i]` with i = 1 once normalised. */
29
+ export type Homography = readonly [number, number, number, number, number, number, number, number, number];
30
+ export declare const IDENTITY: Homography;
31
+ /** The unit card rectangle's corners, in `Quad` order. */
32
+ export declare const UNIT_QUAD: Quad;
33
+ /**
34
+ * The homography that maps the unit card rectangle onto `quad`.
35
+ *
36
+ * Throws `RangeError` when no proper homography exists: a non-finite corner,
37
+ * three collinear corners (zero area), or a quad that is not convex — a
38
+ * concave or self-crossing quad would need the projective plane to fold, and
39
+ * the label would be drawn through the horizon.
40
+ */
41
+ export declare function unitSquareToQuad(quad: Quad): Homography;
42
+ /** Maps a point through `m`. Throws `RangeError` for a point on the vanishing line. */
43
+ export declare function applyHomography(m: Homography, p: Point): Point;
44
+ /** `a · b` (apply `b` first, then `a`), normalised. */
45
+ export declare function multiplyHomography(a: Homography, b: Homography): Homography;
46
+ /** The inverse map, normalised. Throws `RangeError` for a singular matrix. */
47
+ export declare function invertHomography(m: Homography): Homography;
48
+ /** Scales `m` so its bottom-right entry is 1. */
49
+ export declare function normalise(m: readonly number[]): Homography;
50
+ export interface ElementSize {
51
+ /** The label element's layout width in CSS pixels (its `offsetWidth`). */
52
+ readonly width: number;
53
+ /** The label element's layout height in CSS pixels (its `offsetHeight`). */
54
+ readonly height: number;
55
+ }
56
+ /**
57
+ * The CSS `matrix3d(...)` that draws an element of `size` onto the quad that
58
+ * `m` maps the unit card to.
59
+ *
60
+ * The element must be absolutely positioned at the origin of the coordinate
61
+ * space the corners were measured in, with `transform-origin: 0 0`. Its own
62
+ * box (0..width, 0..height) is first scaled to the unit square, then sent
63
+ * through `m`. The 3×3 homography becomes the 4×4
64
+ *
65
+ * | a b 0 c |
66
+ * | d e 0 f |
67
+ * | 0 0 1 0 |
68
+ * | g h 0 i |
69
+ *
70
+ * which leaves z alone, and CSS takes it in COLUMN-major order.
71
+ */
72
+ export declare function toCssMatrix3d(m: Homography, size?: ElementSize): string;
73
+ /** Convenience for the common case: corners in, CSS transform out. */
74
+ export declare function quadToCssMatrix3d(quad: Quad, size?: ElementSize): string;
75
+ //# sourceMappingURL=homography.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homography.d.ts","sourceRoot":"","sources":["../../src/geometry/homography.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;AAExD,sFAAsF;AACtF,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAE1G,eAAO,MAAM,QAAQ,EAAE,UAAwC,CAAA;AAE/D,0DAA0D;AAC1D,eAAO,MAAM,SAAS,EAAE,IAKvB,CAAA;AAOD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAwCvD;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAK9D;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAI3E;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAY1D;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAI1D;AAED,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,GAAE,WAAqC,GAAG,MAAM,CAMhG;AAUD,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAExE"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Planar homographies for AR labels: the projective map from a card's own
3
+ * unit rectangle to the four corners the detector found on screen, and its
4
+ * CSS `matrix3d` form, so a host page can lay a DOM element on the card's
5
+ * plane with nothing but `transform`.
6
+ *
7
+ * Conventions, fixed for every consumer:
8
+ *
9
+ * - Card space is the unit square: (0,0) top-left, (1,0) top-right, (1,1)
10
+ * bottom-right, (0,1) bottom-left of the card as printed (portrait).
11
+ * - Screen space is CSS pixels of the element the corners were measured in,
12
+ * y pointing down.
13
+ * - A `Quad` lists the screen corners in that same order: TL, TR, BR, BL.
14
+ * - A `Homography` is a 3×3 matrix in ROW-MAJOR order, normalised so the
15
+ * bottom-right entry is 1. It maps (u, v, 1) to (x·w, y·w, w).
16
+ *
17
+ * The square-to-quad solution is the closed form from P. Heckbert,
18
+ * "Fundamentals of Texture Mapping and Image Warping" (1989), §2.2.3: no
19
+ * linear solve, exact for any convex quad, and cheap enough to run per track
20
+ * per frame.
21
+ */
22
+ export const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
23
+ /** The unit card rectangle's corners, in `Quad` order. */
24
+ export const UNIT_QUAD = [
25
+ { x: 0, y: 0 },
26
+ { x: 1, y: 0 },
27
+ { x: 1, y: 1 },
28
+ { x: 0, y: 1 },
29
+ ];
30
+ /** Relative tolerance below which a quad or a matrix counts as degenerate. */
31
+ const DEGENERATE_EPS = 1e-12;
32
+ const isFiniteQuad = (q) => q.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
33
+ /**
34
+ * The homography that maps the unit card rectangle onto `quad`.
35
+ *
36
+ * Throws `RangeError` when no proper homography exists: a non-finite corner,
37
+ * three collinear corners (zero area), or a quad that is not convex — a
38
+ * concave or self-crossing quad would need the projective plane to fold, and
39
+ * the label would be drawn through the horizon.
40
+ */
41
+ export function unitSquareToQuad(quad) {
42
+ if (!isFiniteQuad(quad))
43
+ throw new RangeError('quad has a non-finite corner');
44
+ const [p0, p1, p2, p3] = quad;
45
+ const dx1 = p1.x - p2.x;
46
+ const dx2 = p3.x - p2.x;
47
+ const dy1 = p1.y - p2.y;
48
+ const dy2 = p3.y - p2.y;
49
+ const sx = p0.x - p1.x + p2.x - p3.x;
50
+ const sy = p0.y - p1.y + p2.y - p3.y;
51
+ const span = Math.max(...quad.map((p) => Math.abs(p.x - p0.x)), ...quad.map((p) => Math.abs(p.y - p0.y)));
52
+ const den = dx1 * dy2 - dx2 * dy1;
53
+ if (span === 0 || Math.abs(den) <= DEGENERATE_EPS * span * span) {
54
+ throw new RangeError('quad is degenerate (zero area or collinear corners)');
55
+ }
56
+ const g = (sx * dy2 - dx2 * sy) / den;
57
+ const h = (dx1 * sy - sx * dy1) / den;
58
+ // w at each corner: (0,0) → 1, (1,0) → 1+g, (1,1) → 1+g+h, (0,1) → 1+h.
59
+ // All must share a sign, or the quad is not the image of a convex square.
60
+ if (1 + g <= DEGENERATE_EPS || 1 + h <= DEGENERATE_EPS || 1 + g + h <= DEGENERATE_EPS) {
61
+ throw new RangeError('quad is not convex');
62
+ }
63
+ return [
64
+ p1.x - p0.x + g * p1.x,
65
+ p3.x - p0.x + h * p3.x,
66
+ p0.x,
67
+ p1.y - p0.y + g * p1.y,
68
+ p3.y - p0.y + h * p3.y,
69
+ p0.y,
70
+ g,
71
+ h,
72
+ 1,
73
+ ];
74
+ }
75
+ /** Maps a point through `m`. Throws `RangeError` for a point on the vanishing line. */
76
+ export function applyHomography(m, p) {
77
+ const [a, b, c, d, e, f, g, h, i] = m;
78
+ const w = g * p.x + h * p.y + i;
79
+ if (Math.abs(w) <= DEGENERATE_EPS)
80
+ throw new RangeError('point maps to infinity');
81
+ return { x: (a * p.x + b * p.y + c) / w, y: (d * p.x + e * p.y + f) / w };
82
+ }
83
+ /** `a · b` (apply `b` first, then `a`), normalised. */
84
+ export function multiplyHomography(a, b) {
85
+ const r = (row, col) => a[row * 3] * b[col] + a[row * 3 + 1] * b[3 + col] + a[row * 3 + 2] * b[6 + col];
86
+ return normalise([r(0, 0), r(0, 1), r(0, 2), r(1, 0), r(1, 1), r(1, 2), r(2, 0), r(2, 1), r(2, 2)]);
87
+ }
88
+ /** The inverse map, normalised. Throws `RangeError` for a singular matrix. */
89
+ export function invertHomography(m) {
90
+ const [a, b, c, d, e, f, g, h, i] = m;
91
+ const A = e * i - f * h;
92
+ const B = -(d * i - f * g);
93
+ const C = d * h - e * g;
94
+ const det = a * A + b * B + c * C;
95
+ const scale = Math.max(...m.map(Math.abs));
96
+ if (!Number.isFinite(det) || Math.abs(det) <= DEGENERATE_EPS * scale * scale * scale) {
97
+ throw new RangeError('homography is singular');
98
+ }
99
+ // Adjugate (transposed cofactors); the 1/det factor cancels in normalise().
100
+ return normalise([A, -(b * i - c * h), b * f - c * e, B, a * i - c * g, -(a * f - c * d), C, -(a * h - b * g), a * e - b * d]);
101
+ }
102
+ /** Scales `m` so its bottom-right entry is 1. */
103
+ export function normalise(m) {
104
+ const k = m[8];
105
+ if (Math.abs(k) <= DEGENERATE_EPS)
106
+ throw new RangeError('homography cannot be normalised (h33 = 0)');
107
+ return m.map((v) => v / k);
108
+ }
109
+ /**
110
+ * The CSS `matrix3d(...)` that draws an element of `size` onto the quad that
111
+ * `m` maps the unit card to.
112
+ *
113
+ * The element must be absolutely positioned at the origin of the coordinate
114
+ * space the corners were measured in, with `transform-origin: 0 0`. Its own
115
+ * box (0..width, 0..height) is first scaled to the unit square, then sent
116
+ * through `m`. The 3×3 homography becomes the 4×4
117
+ *
118
+ * | a b 0 c |
119
+ * | d e 0 f |
120
+ * | 0 0 1 0 |
121
+ * | g h 0 i |
122
+ *
123
+ * which leaves z alone, and CSS takes it in COLUMN-major order.
124
+ */
125
+ export function toCssMatrix3d(m, size = { width: 1, height: 1 }) {
126
+ if (!(size.width > 0) || !(size.height > 0))
127
+ throw new RangeError('element size must be positive');
128
+ const scaled = multiplyHomography(m, [1 / size.width, 0, 0, 0, 1 / size.height, 0, 0, 0, 1]);
129
+ const [a, b, c, d, e, f, g, h, i] = scaled;
130
+ const columnMajor = [a, d, 0, g, b, e, 0, h, 0, 0, 1, 0, c, f, 0, i];
131
+ return `matrix3d(${columnMajor.map(formatCssNumber).join(', ')})`;
132
+ }
133
+ /** Fixed precision, no `-0`, no exponent: what every engine parses identically. */
134
+ function formatCssNumber(v) {
135
+ if (!Number.isFinite(v))
136
+ throw new RangeError('non-finite matrix entry');
137
+ const fixed = Number(v.toFixed(12));
138
+ if (fixed === 0)
139
+ return '0';
140
+ return fixed.toFixed(12).replace(/\.?0+$/, '');
141
+ }
142
+ /** Convenience for the common case: corners in, CSS transform out. */
143
+ export function quadToCssMatrix3d(quad, size) {
144
+ return toCssMatrix3d(unitSquareToQuad(quad), size);
145
+ }
146
+ //# sourceMappingURL=homography.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homography.js","sourceRoot":"","sources":["../../src/geometry/homography.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { ManifestFile } from './manifest.js';
2
+ /** The Cache API cache the SDK owns. Bump the suffix if the key scheme changes. */
3
+ export declare const CACHE_NAME = "dejadex-assets-v1";
4
+ export declare const MANIFEST_TIMEOUT_MS = 10000;
5
+ export type FetchLike = (input: string, init: {
6
+ signal: AbortSignal;
7
+ cache?: RequestCache;
8
+ }) => Promise<Response>;
9
+ /** The subset of `CacheStorage` used, so tests and hosts without one can pass their own (or none). */
10
+ export interface CacheLike {
11
+ match(key: string): Promise<Response | undefined>;
12
+ put(key: string, response: Response): Promise<void>;
13
+ delete(key: string): Promise<boolean>;
14
+ keys(): Promise<readonly {
15
+ readonly url: string;
16
+ }[]>;
17
+ }
18
+ export interface AssetStoreOptions {
19
+ readonly fetch: FetchLike;
20
+ /** null: no cache (every start downloads). */
21
+ readonly cache: CacheLike | null;
22
+ /** Overrides the per-file bound (ms); default from the size, at ≥ 100 kB/s. */
23
+ readonly fileTimeoutMs?: number;
24
+ }
25
+ export interface AssetStore {
26
+ /** The file's bytes, from the cache or the network, verified. */
27
+ get(file: ManifestFile, signal?: AbortSignal): Promise<ArrayBuffer>;
28
+ /** The manifest document from `url`; offline, the last one that loaded from it. */
29
+ manifest(url: string, signal?: AbortSignal): Promise<unknown>;
30
+ /** Drops cached files whose digest is not in `keep` (after a new manifest verified). */
31
+ evictExcept(keep: readonly string[]): Promise<number>;
32
+ }
33
+ export declare function sha256Hex(data: ArrayBuffer): Promise<string>;
34
+ /** `AbortSignal.any` where the runtime has it, else a controller following both. */
35
+ export declare function anySignal(signals: readonly (AbortSignal | undefined)[]): AbortSignal;
36
+ export declare function createAssetStore(options: AssetStoreOptions): AssetStore;
37
+ /** The page's Cache API cache, or null where there is none (or it is blocked). */
38
+ export declare function openDefaultCache(): Promise<CacheLike | null>;
39
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/identify/assets.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,mFAAmF;AACnF,eAAO,MAAM,UAAU,sBAAsB,CAAA;AAO7C,eAAO,MAAM,mBAAmB,QAAS,CAAA;AAEzC,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,YAAY,CAAA;CAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjH,sGAAsG;AACtG,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAA;IACjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,IAAI,OAAO,CAAC,SAAS;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAA;CACrD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACnE,mFAAmF;IACnF,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7D,wFAAwF;IACxF,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACtD;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlE;AASD,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,SAAS,CAAC,EAAE,GAAG,WAAW,CAapF;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CA0FvE;AAED,kFAAkF;AAClF,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAalE"}