@forgeax/engine-input 0.0.0-dev.8d955ade1c79

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 (107) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +312 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/browser-backend-dual-instance.test.d.ts +2 -0
  5. package/dist/__tests__/browser-backend-dual-instance.test.d.ts.map +1 -0
  6. package/dist/__tests__/browser-backend-focus-loss.test.d.ts +2 -0
  7. package/dist/__tests__/browser-backend-focus-loss.test.d.ts.map +1 -0
  8. package/dist/__tests__/browser-backend-lock-error.test.d.ts +2 -0
  9. package/dist/__tests__/browser-backend-lock-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/browser-backend-lock-gate.test.d.ts +2 -0
  11. package/dist/__tests__/browser-backend-lock-gate.test.d.ts.map +1 -0
  12. package/dist/__tests__/browser-backend-lock-release.test.d.ts +2 -0
  13. package/dist/__tests__/browser-backend-lock-release.test.d.ts.map +1 -0
  14. package/dist/__tests__/browser-backend-lock-state.test.d.ts +2 -0
  15. package/dist/__tests__/browser-backend-lock-state.test.d.ts.map +1 -0
  16. package/dist/__tests__/canvas-input-boundary.test.d.ts +2 -0
  17. package/dist/__tests__/canvas-input-boundary.test.d.ts.map +1 -0
  18. package/dist/__tests__/canvas-input-owner.test-d.d.ts +2 -0
  19. package/dist/__tests__/canvas-input-owner.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/composite-backend.test.d.ts +2 -0
  21. package/dist/__tests__/composite-backend.test.d.ts.map +1 -0
  22. package/dist/__tests__/controller-platform-owner.test-d.d.ts +2 -0
  23. package/dist/__tests__/controller-platform-owner.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/input-snapshot.test-d.d.ts +2 -0
  25. package/dist/__tests__/input-snapshot.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/input-snapshot.test.d.ts +2 -0
  27. package/dist/__tests__/input-snapshot.test.d.ts.map +1 -0
  28. package/dist/__tests__/swipe-direction-owner.test-d.d.ts +2 -0
  29. package/dist/__tests__/swipe-direction-owner.test-d.d.ts.map +1 -0
  30. package/dist/__tests__/ui-input-matrix.test.d.ts +2 -0
  31. package/dist/__tests__/ui-input-matrix.test.d.ts.map +1 -0
  32. package/dist/__tests__/ui-ownership-edge.test.d.ts +2 -0
  33. package/dist/__tests__/ui-ownership-edge.test.d.ts.map +1 -0
  34. package/dist/__tests__/ui-ownership.test.d.ts +2 -0
  35. package/dist/__tests__/ui-ownership.test.d.ts.map +1 -0
  36. package/dist/__tests__/ui-reset-idempotence.test.d.ts +2 -0
  37. package/dist/__tests__/ui-reset-idempotence.test.d.ts.map +1 -0
  38. package/dist/__tests__/ui-reset.test.d.ts +2 -0
  39. package/dist/__tests__/ui-reset.test.d.ts.map +1 -0
  40. package/dist/action-state.d.ts +146 -0
  41. package/dist/action-state.d.ts.map +1 -0
  42. package/dist/browser-backend.d.ts +121 -0
  43. package/dist/browser-backend.d.ts.map +1 -0
  44. package/dist/canvas-input-boundary.d.ts +13 -0
  45. package/dist/canvas-input-boundary.d.ts.map +1 -0
  46. package/dist/composite-backend.d.ts +34 -0
  47. package/dist/composite-backend.d.ts.map +1 -0
  48. package/dist/controller-db-data.d.ts +5 -0
  49. package/dist/controller-db-data.d.ts.map +1 -0
  50. package/dist/controller-db-data.mjs +12 -0
  51. package/dist/controller-db-data.mjs.map +1 -0
  52. package/dist/controller-db.d.ts +70 -0
  53. package/dist/controller-db.d.ts.map +1 -0
  54. package/dist/controller-db.mjs +119 -0
  55. package/dist/controller-db.mjs.map +1 -0
  56. package/dist/frame-start-scan-system.d.ts +37 -0
  57. package/dist/frame-start-scan-system.d.ts.map +1 -0
  58. package/dist/gamepad-frame.d.ts +42 -0
  59. package/dist/gamepad-frame.d.ts.map +1 -0
  60. package/dist/gesture-recognizer.d.ts +145 -0
  61. package/dist/gesture-recognizer.d.ts.map +1 -0
  62. package/dist/index.d.ts +13 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.mjs +1502 -0
  65. package/dist/index.mjs.map +1 -0
  66. package/dist/input-snapshot.d.ts +420 -0
  67. package/dist/input-snapshot.d.ts.map +1 -0
  68. package/dist/plugin-service.d.ts +11 -0
  69. package/dist/plugin-service.d.ts.map +1 -0
  70. package/dist/ui-ownership.d.ts +17 -0
  71. package/dist/ui-ownership.d.ts.map +1 -0
  72. package/dist/virtual-joystick.d.ts +39 -0
  73. package/dist/virtual-joystick.d.ts.map +1 -0
  74. package/package.json +72 -0
  75. package/src/__tests__/browser-backend-dual-instance.test.ts +379 -0
  76. package/src/__tests__/browser-backend-focus-loss.test.ts +239 -0
  77. package/src/__tests__/browser-backend-lock-error.test.ts +425 -0
  78. package/src/__tests__/browser-backend-lock-gate.test.ts +283 -0
  79. package/src/__tests__/browser-backend-lock-release.test.ts +641 -0
  80. package/src/__tests__/browser-backend-lock-state.test.ts +604 -0
  81. package/src/__tests__/canvas-input-boundary.test.ts +65 -0
  82. package/src/__tests__/canvas-input-owner.test-d.ts +39 -0
  83. package/src/__tests__/composite-backend.test.ts +299 -0
  84. package/src/__tests__/controller-platform-owner.test-d.ts +46 -0
  85. package/src/__tests__/input-snapshot.test-d.ts +78 -0
  86. package/src/__tests__/input-snapshot.test.ts +96 -0
  87. package/src/__tests__/swipe-direction-owner.test-d.ts +45 -0
  88. package/src/__tests__/ui-input-matrix.test.ts +31 -0
  89. package/src/__tests__/ui-ownership-edge.test.ts +33 -0
  90. package/src/__tests__/ui-ownership.test.ts +35 -0
  91. package/src/__tests__/ui-reset-idempotence.test.ts +17 -0
  92. package/src/__tests__/ui-reset.test.ts +25 -0
  93. package/src/action-state.ts +380 -0
  94. package/src/browser-backend.ts +884 -0
  95. package/src/canvas-input-boundary.ts +81 -0
  96. package/src/composite-backend.ts +189 -0
  97. package/src/controller-db-data.ts +25 -0
  98. package/src/controller-db.ts +218 -0
  99. package/src/frame-start-scan-system.ts +93 -0
  100. package/src/gamepad-frame.ts +259 -0
  101. package/src/gesture-recognizer.ts +495 -0
  102. package/src/index.ts +101 -0
  103. package/src/input-snapshot.ts +732 -0
  104. package/src/plugin-service.ts +31 -0
  105. package/src/ui-ownership.ts +61 -0
  106. package/src/virtual-joystick.ts +103 -0
  107. package/vendor/gamecontrollerdb.txt +2114 -0
@@ -0,0 +1,70 @@
1
+ /** A single mapping token target: physical button / axis / hat index. */
2
+ export type MappingToken = {
3
+ readonly kind: 'button';
4
+ readonly index: number;
5
+ } | {
6
+ readonly kind: 'axis';
7
+ readonly index: number;
8
+ readonly half?: '+' | '-';
9
+ } | {
10
+ readonly kind: 'hat';
11
+ readonly index: number;
12
+ readonly mask: number;
13
+ };
14
+ /**
15
+ * Parsed mapping tokens for one controller entry, keyed by the SDL logical
16
+ * name (`a`, `b`, `leftx`, `dpup`, `lefttrigger`, ...). Values are the
17
+ * physical source (button `bN`, axis `aN`, hat `hN.M`).
18
+ */
19
+ export type MappingTokens = Readonly<Record<string, MappingToken>>;
20
+ /** One controller-DB row: its platform section + parsed mapping tokens. */
21
+ export interface ControllerDbEntry {
22
+ readonly platform: string | undefined;
23
+ readonly tokens: MappingTokens;
24
+ }
25
+ /**
26
+ * Parsed controller DB: SDL GUID (32-char hex) -> one entry per platform
27
+ * section. The same GUID can appear in multiple platform sections, so the
28
+ * value is an array.
29
+ */
30
+ export type ControllerDb = Readonly<Record<string, readonly ControllerDbEntry[]>>;
31
+ /** Recognised web platform section labels (D-13). */
32
+ declare const CONTROLLER_PLATFORMS: readonly ["Windows", "Mac OS X", "Linux", "Android", "iOS"];
33
+ export type ControllerPlatform = (typeof CONTROLLER_PLATFORMS)[number];
34
+ /**
35
+ * Parse a gamecontrollerdb.txt string into a ControllerDb map. Comment
36
+ * (`#`) and blank lines are skipped. Each data line is
37
+ * `GUID,Name,token:source,...,platform:PLATFORM,`.
38
+ */
39
+ export declare function parseControllerDb(txt: string): ControllerDb;
40
+ /**
41
+ * Build a 32-char SDL GUID hex from a VID/PID pair (D-13 strategy 2):
42
+ * bus (default USB 0x03) + CRC=0 + VID + fill + PID + fill + version=0 +
43
+ * driver sig/data=0. Each 16-bit field is little-endian within the string.
44
+ *
45
+ * Example: buildGuidFromVidPid(0x045e, 0x028e) ->
46
+ * '030000005e0400008e02000000000000' (Xbox 360).
47
+ */
48
+ export declare function buildGuidFromVidPid(vid: number, pid: number, bus?: number): string;
49
+ /**
50
+ * Extract an SDL GUID from a browser `Gamepad.id` string. Handles the
51
+ * Chrome/Edge (`Vendor: XXXX Product: YYYY`) and Firefox (`VID-PID-Name`,
52
+ * leading-zero tolerant) formats. Returns undefined when VID/PID cannot be
53
+ * extracted (Safari name-only strings, XInput devices) so the caller keeps
54
+ * the Feat1 empty-signal (graceful degradation, R-3).
55
+ */
56
+ export declare function extractGuidFromGamepadId(id: string): string | undefined;
57
+ /**
58
+ * Map a `navigator.userAgent` string to a gamecontrollerdb.txt platform
59
+ * section label (D-13). Returns undefined when the UA is unrecognised, in
60
+ * which case the caller falls back to any-platform entry selection.
61
+ */
62
+ export declare function platformFromUserAgent(ua: string): ControllerPlatform | undefined;
63
+ /**
64
+ * Select the best mapping entry for a GUID: prefer the entry whose platform
65
+ * section matches `platform`, else fall back to the first available entry
66
+ * (any platform). Returns undefined when the GUID is absent from the DB.
67
+ */
68
+ export declare function selectBestMappingEntry(db: ControllerDb, guid: string, platform: string | undefined): ControllerDbEntry | undefined;
69
+ export {};
70
+ //# sourceMappingURL=controller-db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller-db.d.ts","sourceRoot":"","sources":["../src/controller-db.ts"],"names":[],"mappings":"AAeA,yEAAyE;AACzE,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAA;CAAE,GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAEnE,2EAA2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC,CAAC,CAAC;AAElF,qDAAqD;AACrD,QAAA,MAAM,oBAAoB,6DAA8D,CAAC;AACzF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAgCvE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAmC3D;AASD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,SAAO,GAAG,MAAM,CAWhF;AAKD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA2BvE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAShF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,iBAAiB,GAAG,SAAS,CAQ/B"}
@@ -0,0 +1,119 @@
1
+ // src/controller-db.ts
2
+ var CONTROLLER_PLATFORMS = ["Windows", "Mac OS X", "Linux", "Android", "iOS"];
3
+ function parseMappingToken(source) {
4
+ let half;
5
+ let body = source;
6
+ if (body.startsWith("+") || body.startsWith("-")) {
7
+ half = body[0];
8
+ body = body.slice(1);
9
+ }
10
+ const prefix = body[0];
11
+ if (prefix === "b") {
12
+ const index = Number.parseInt(body.slice(1), 10);
13
+ return Number.isNaN(index) ? void 0 : { kind: "button", index };
14
+ }
15
+ if (prefix === "a") {
16
+ const index = Number.parseInt(body.slice(1), 10);
17
+ if (Number.isNaN(index)) return void 0;
18
+ return half ? { kind: "axis", index, half } : { kind: "axis", index };
19
+ }
20
+ if (prefix === "h") {
21
+ const dot = body.indexOf(".");
22
+ if (dot < 0) return void 0;
23
+ const index = Number.parseInt(body.slice(1, dot), 10);
24
+ const mask = Number.parseInt(body.slice(dot + 1), 10);
25
+ return Number.isNaN(index) || Number.isNaN(mask) ? void 0 : { kind: "hat", index, mask };
26
+ }
27
+ return void 0;
28
+ }
29
+ function parseControllerDb(txt) {
30
+ const db = {};
31
+ for (const rawLine of txt.split("\n")) {
32
+ const line = rawLine.trim();
33
+ if (line.length === 0 || line.startsWith("#")) continue;
34
+ const fields = line.split(",");
35
+ const guid = fields[0];
36
+ if (!guid || guid.length !== 32) continue;
37
+ let platform;
38
+ const tokens = {};
39
+ for (let i = 2; i < fields.length; i++) {
40
+ const field = fields[i];
41
+ if (!field) continue;
42
+ const colon = field.indexOf(":");
43
+ if (colon < 0) continue;
44
+ const key = field.slice(0, colon);
45
+ const source = field.slice(colon + 1);
46
+ if (key === "platform") {
47
+ platform = source;
48
+ continue;
49
+ }
50
+ const token = parseMappingToken(source);
51
+ if (token) tokens[key] = token;
52
+ }
53
+ const entry = { platform, tokens };
54
+ const bucket = db[guid];
55
+ if (bucket) {
56
+ bucket.push(entry);
57
+ } else {
58
+ db[guid] = [entry];
59
+ }
60
+ }
61
+ return db;
62
+ }
63
+ function le16Hex(value) {
64
+ const lo = value & 255;
65
+ const hi = value >> 8 & 255;
66
+ return lo.toString(16).padStart(2, "0") + hi.toString(16).padStart(2, "0");
67
+ }
68
+ function buildGuidFromVidPid(vid, pid, bus = 3) {
69
+ return le16Hex(bus) + // bytes 0-1: bus type
70
+ "0000" + // bytes 2-3: CRC = 0
71
+ le16Hex(vid) + // bytes 4-5: VID (LE)
72
+ "0000" + // bytes 6-7: fill
73
+ le16Hex(pid) + // bytes 8-9: PID (LE)
74
+ "000000000000";
75
+ }
76
+ var CHROME_VENDOR_RE = /Vendor:\s*([0-9a-f]{1,4})/i;
77
+ var CHROME_PRODUCT_RE = /Product:\s*([0-9a-f]{1,4})/i;
78
+ function extractGuidFromGamepadId(id) {
79
+ if (id.toLowerCase().includes("xinput")) return void 0;
80
+ const vendorMatch = id.match(CHROME_VENDOR_RE);
81
+ const productMatch = id.match(CHROME_PRODUCT_RE);
82
+ if (vendorMatch?.[1] && productMatch?.[1]) {
83
+ const vid = Number.parseInt(vendorMatch[1], 16);
84
+ const pid = Number.parseInt(productMatch[1], 16);
85
+ if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);
86
+ }
87
+ const parts = id.split("-");
88
+ if (parts.length >= 3 && parts[0] && parts[1]) {
89
+ const vidHex = parts[0];
90
+ const pidHex = parts[1];
91
+ if (/^[0-9a-f]{1,4}$/i.test(vidHex) && /^[0-9a-f]{1,4}$/i.test(pidHex)) {
92
+ const vid = Number.parseInt(vidHex, 16);
93
+ const pid = Number.parseInt(pidHex, 16);
94
+ if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);
95
+ }
96
+ }
97
+ return void 0;
98
+ }
99
+ function platformFromUserAgent(ua) {
100
+ if (/iPhone|iPad|iPod/.test(ua)) return CONTROLLER_PLATFORMS[4];
101
+ if (/Android/.test(ua)) return CONTROLLER_PLATFORMS[3];
102
+ if (/Windows/.test(ua)) return CONTROLLER_PLATFORMS[0];
103
+ if (/Mac OS X|Macintosh/.test(ua)) return CONTROLLER_PLATFORMS[1];
104
+ if (/Linux/.test(ua)) return CONTROLLER_PLATFORMS[2];
105
+ return void 0;
106
+ }
107
+ function selectBestMappingEntry(db, guid, platform) {
108
+ const entries = db[guid];
109
+ if (!entries || entries.length === 0) return void 0;
110
+ if (platform) {
111
+ const match = entries.find((e) => e.platform === platform);
112
+ if (match) return match;
113
+ }
114
+ return entries[0];
115
+ }
116
+
117
+ export { buildGuidFromVidPid, extractGuidFromGamepadId, parseControllerDb, platformFromUserAgent, selectBestMappingEntry };
118
+ //# sourceMappingURL=controller-db.mjs.map
119
+ //# sourceMappingURL=controller-db.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/controller-db.ts"],"names":[],"mappings":"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","file":"controller-db.mjs","sourcesContent":["// controller-db.ts -- pure SDL_GameControllerDB parsing + GUID derivation.\n//\n// This module holds ONLY pure functions (no DOM, no ECS, no vendored data\n// import). The 554KB gamecontrollerdb.txt lives in the separate\n// `./controller-db-data` sub-export so it never enters the main-entry\n// bundle; the backend dynamic-imports both this module and the data module\n// on first sight of a non-standard gamepad (D-2 lazy-load).\n//\n// SDL GUID reference: KB source\n// .forgeax-harness/knowledge-base/sources/2026-07-07-sdl-guid-browser-gamepad-id-mapping.md\n//\n// D-13 strategy 2: build a match GUID from VID/PID with bus=USB(0x03),\n// CRC=0, version=0, driver signature/data=0. This matches the vast\n// majority of gamecontrollerdb.txt native entries directly.\n\n/** A single mapping token target: physical button / axis / hat index. */\nexport type MappingToken =\n | { readonly kind: 'button'; readonly index: number }\n | { readonly kind: 'axis'; readonly index: number; readonly half?: '+' | '-' }\n | { readonly kind: 'hat'; readonly index: number; readonly mask: number };\n\n/**\n * Parsed mapping tokens for one controller entry, keyed by the SDL logical\n * name (`a`, `b`, `leftx`, `dpup`, `lefttrigger`, ...). Values are the\n * physical source (button `bN`, axis `aN`, hat `hN.M`).\n */\nexport type MappingTokens = Readonly<Record<string, MappingToken>>;\n\n/** One controller-DB row: its platform section + parsed mapping tokens. */\nexport interface ControllerDbEntry {\n readonly platform: string | undefined;\n readonly tokens: MappingTokens;\n}\n\n/**\n * Parsed controller DB: SDL GUID (32-char hex) -> one entry per platform\n * section. The same GUID can appear in multiple platform sections, so the\n * value is an array.\n */\nexport type ControllerDb = Readonly<Record<string, readonly ControllerDbEntry[]>>;\n\n/** Recognised web platform section labels (D-13). */\nconst CONTROLLER_PLATFORMS = ['Windows', 'Mac OS X', 'Linux', 'Android', 'iOS'] as const;\nexport type ControllerPlatform = (typeof CONTROLLER_PLATFORMS)[number];\n\n/** Parse one `key:source` mapping token into a MappingToken. */\nfunction parseMappingToken(source: string): MappingToken | undefined {\n // Half-axis prefix: +aN / -aN.\n let half: '+' | '-' | undefined;\n let body = source;\n if (body.startsWith('+') || body.startsWith('-')) {\n half = body[0] as '+' | '-';\n body = body.slice(1);\n }\n const prefix = body[0];\n if (prefix === 'b') {\n const index = Number.parseInt(body.slice(1), 10);\n return Number.isNaN(index) ? undefined : { kind: 'button', index };\n }\n if (prefix === 'a') {\n const index = Number.parseInt(body.slice(1), 10);\n if (Number.isNaN(index)) return undefined;\n return half ? { kind: 'axis', index, half } : { kind: 'axis', index };\n }\n if (prefix === 'h') {\n // hN.M -- hat N, direction mask M.\n const dot = body.indexOf('.');\n if (dot < 0) return undefined;\n const index = Number.parseInt(body.slice(1, dot), 10);\n const mask = Number.parseInt(body.slice(dot + 1), 10);\n return Number.isNaN(index) || Number.isNaN(mask) ? undefined : { kind: 'hat', index, mask };\n }\n return undefined;\n}\n\n/**\n * Parse a gamecontrollerdb.txt string into a ControllerDb map. Comment\n * (`#`) and blank lines are skipped. Each data line is\n * `GUID,Name,token:source,...,platform:PLATFORM,`.\n */\nexport function parseControllerDb(txt: string): ControllerDb {\n const db: Record<string, ControllerDbEntry[]> = {};\n for (const rawLine of txt.split('\\n')) {\n const line = rawLine.trim();\n if (line.length === 0 || line.startsWith('#')) continue;\n const fields = line.split(',');\n const guid = fields[0];\n if (!guid || guid.length !== 32) continue;\n let platform: string | undefined;\n const tokens: Record<string, MappingToken> = {};\n // fields[1] is the human-readable name; mapping tokens start at index 2.\n for (let i = 2; i < fields.length; i++) {\n const field = fields[i];\n if (!field) continue;\n const colon = field.indexOf(':');\n if (colon < 0) continue;\n const key = field.slice(0, colon);\n const source = field.slice(colon + 1);\n if (key === 'platform') {\n platform = source;\n continue;\n }\n // crc / sdk / hint fields are not physical mappings; skip non b/a/h.\n const token = parseMappingToken(source);\n if (token) tokens[key] = token;\n }\n const entry: ControllerDbEntry = { platform, tokens };\n const bucket = db[guid];\n if (bucket) {\n bucket.push(entry);\n } else {\n db[guid] = [entry];\n }\n }\n return db;\n}\n\n/** Format a 16-bit value as a 4-char little-endian hex pair. */\nfunction le16Hex(value: number): string {\n const lo = value & 0xff;\n const hi = (value >> 8) & 0xff;\n return lo.toString(16).padStart(2, '0') + hi.toString(16).padStart(2, '0');\n}\n\n/**\n * Build a 32-char SDL GUID hex from a VID/PID pair (D-13 strategy 2):\n * bus (default USB 0x03) + CRC=0 + VID + fill + PID + fill + version=0 +\n * driver sig/data=0. Each 16-bit field is little-endian within the string.\n *\n * Example: buildGuidFromVidPid(0x045e, 0x028e) ->\n * '030000005e0400008e02000000000000' (Xbox 360).\n */\nexport function buildGuidFromVidPid(vid: number, pid: number, bus = 0x03): string {\n return (\n le16Hex(bus) + // bytes 0-1: bus type\n '0000' + // bytes 2-3: CRC = 0\n le16Hex(vid) + // bytes 4-5: VID (LE)\n '0000' + // bytes 6-7: fill\n le16Hex(pid) + // bytes 8-9: PID (LE)\n '0000' + // bytes 10-11: fill\n '0000' + // bytes 12-13: version = 0\n '0000' // bytes 14-15: driver signature + data = 0\n );\n}\n\nconst CHROME_VENDOR_RE = /Vendor:\\s*([0-9a-f]{1,4})/i;\nconst CHROME_PRODUCT_RE = /Product:\\s*([0-9a-f]{1,4})/i;\n\n/**\n * Extract an SDL GUID from a browser `Gamepad.id` string. Handles the\n * Chrome/Edge (`Vendor: XXXX Product: YYYY`) and Firefox (`VID-PID-Name`,\n * leading-zero tolerant) formats. Returns undefined when VID/PID cannot be\n * extracted (Safari name-only strings, XInput devices) so the caller keeps\n * the Feat1 empty-signal (graceful degradation, R-3).\n */\nexport function extractGuidFromGamepadId(id: string): string | undefined {\n // XInput devices expose no VID/PID in either Chrome or Firefox.\n if (id.toLowerCase().includes('xinput')) return undefined;\n\n // Chrome / Edge: \"... (STANDARD GAMEPAD Vendor: 054c Product: 09cc)\".\n const vendorMatch = id.match(CHROME_VENDOR_RE);\n const productMatch = id.match(CHROME_PRODUCT_RE);\n if (vendorMatch?.[1] && productMatch?.[1]) {\n const vid = Number.parseInt(vendorMatch[1], 16);\n const pid = Number.parseInt(productMatch[1], 16);\n if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);\n }\n\n // Firefox: \"046d-c216-Logitech Dual Action\" (VID-PID-Name). The first two\n // dash segments must be pure hex; a name-only Safari string fails this.\n const parts = id.split('-');\n if (parts.length >= 3 && parts[0] && parts[1]) {\n const vidHex = parts[0];\n const pidHex = parts[1];\n if (/^[0-9a-f]{1,4}$/i.test(vidHex) && /^[0-9a-f]{1,4}$/i.test(pidHex)) {\n const vid = Number.parseInt(vidHex, 16);\n const pid = Number.parseInt(pidHex, 16);\n if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);\n }\n }\n\n return undefined;\n}\n\n/**\n * Map a `navigator.userAgent` string to a gamecontrollerdb.txt platform\n * section label (D-13). Returns undefined when the UA is unrecognised, in\n * which case the caller falls back to any-platform entry selection.\n */\nexport function platformFromUserAgent(ua: string): ControllerPlatform | undefined {\n // Order matters: iOS + Android both contain substrings that overlap with\n // the desktop checks, so test the mobile/specific cases first.\n if (/iPhone|iPad|iPod/.test(ua)) return CONTROLLER_PLATFORMS[4];\n if (/Android/.test(ua)) return CONTROLLER_PLATFORMS[3];\n if (/Windows/.test(ua)) return CONTROLLER_PLATFORMS[0];\n if (/Mac OS X|Macintosh/.test(ua)) return CONTROLLER_PLATFORMS[1];\n if (/Linux/.test(ua)) return CONTROLLER_PLATFORMS[2];\n return undefined;\n}\n\n/**\n * Select the best mapping entry for a GUID: prefer the entry whose platform\n * section matches `platform`, else fall back to the first available entry\n * (any platform). Returns undefined when the GUID is absent from the DB.\n */\nexport function selectBestMappingEntry(\n db: ControllerDb,\n guid: string,\n platform: string | undefined,\n): ControllerDbEntry | undefined {\n const entries = db[guid];\n if (!entries || entries.length === 0) return undefined;\n if (platform) {\n const match = entries.find((e) => e.platform === platform);\n if (match) return match;\n }\n return entries[0];\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import { type SystemHandle } from '@forgeax/engine-ecs';
2
+ /**
3
+ * Stable system name (locked by `frame-start-scan-system.test.ts`). User
4
+ * systems reference it through `after: [FRAME_START_SCAN_SYSTEM_NAME]`
5
+ * to ensure they observe the freshly written snapshot.
6
+ */
7
+ export declare const FRAME_START_SCAN_SYSTEM_NAME = "input-frame-start-scan";
8
+ export declare const InputSet: import("@forgeax/engine-ecs").SystemSet;
9
+ /**
10
+ * Resource key under which the {@link InputBackend} producer is inserted
11
+ * (M2 — full resource-ification, D-2 / D-7). The frame-start scan system
12
+ * declares it in `resources`; the fn body reads it back via
13
+ * `world.getResource(INPUT_BACKEND_KEY)`.
14
+ *
15
+ * Aligns with the `INPUT_SNAPSHOT_RESOURCE_KEY` naming. Consumers import the
16
+ * constant rather than the bare string so a typo degrades to an import error
17
+ * (charter P3).
18
+ */
19
+ export declare const INPUT_BACKEND_KEY: "InputBackend";
20
+ /**
21
+ * The frame-start scan system token (M2 — full resource-ification, D-4).
22
+ *
23
+ * Module-level `defineSystem` with the real fn body — no factory, no closure.
24
+ * Each `world.update()` tick:
25
+ * 1. reads the {@link InputBackend} from `INPUT_BACKEND_KEY` and calls
26
+ * `backend.sample()` to drain the per-frame accumulator (movement delta +
27
+ * up-edge set);
28
+ * 2. derives a fresh `InputSnapshot` via `snapshotFromSample`;
29
+ * 3. writes it under `INPUT_SNAPSHOT_RESOURCE_KEY` via
30
+ * `world.insertResource` -- idempotent overwrite, charter P4
31
+ * consistent abstraction (consumers always read the same Resource key
32
+ * regardless of which backend produced the sample).
33
+ *
34
+ * Labelled `'input'` (spec §6.2 label-anchor map).
35
+ */
36
+ export declare const InputFrameStartScan: SystemHandle<readonly []>;
37
+ //# sourceMappingURL=frame-start-scan-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-start-scan-system.d.ts","sourceRoot":"","sources":["../src/frame-start-scan-system.ts"],"names":[],"mappings":"AAWA,OAAO,EAAiC,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAevF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,2BAA2B,CAAC;AACrE,eAAO,MAAM,QAAQ,yCAAqC,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAG,cAAuB,CAAC;AAEzD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAAY,CAAC,SAAS,EAAE,CA8BxD,CAAC"}
@@ -0,0 +1,42 @@
1
+ import type { MappingTokens } from './controller-db';
2
+ import type { GamepadSlotSample } from './input-snapshot';
3
+ /**
4
+ * Minimal raw Gamepad shape from browser API (no DOM types here).
5
+ *
6
+ * M3 F-2 seam fix: `id` mirrors the browser `Gamepad.id` string so the
7
+ * remap lookup can derive the SDL GUID keyed by device identity (not by
8
+ * slot index, which is unstable across reconnects).
9
+ */
10
+ export interface RawGamepadStub {
11
+ readonly index: number;
12
+ readonly id: string;
13
+ readonly connected: boolean;
14
+ readonly mapping: string;
15
+ readonly buttons: readonly {
16
+ readonly value: number;
17
+ readonly pressed: boolean;
18
+ }[];
19
+ readonly axes: readonly number[];
20
+ }
21
+ /**
22
+ * Diff prev vs cur frames for all gamepad slots. Returns a flat
23
+ * GamepadSlotSample[] array keyed by gamepad.index.
24
+ *
25
+ * - Standard-mapping slots produce GamepadSlotSample with edge sets filled
26
+ * from prev vs cur pressed-set delta.
27
+ * - Non-standard slots: when `remapLookup` returns mapping tokens for the
28
+ * gamepad id, raw HID indices are re-projected onto the standard layout
29
+ * and the slot reports standardMapping=true (D-1 semantic redefinition).
30
+ * With no lookup or no match, the slot reports standardMapping=false with
31
+ * empty readpoints and connected=true (Feat1 behaviour, AC-04).
32
+ * - Disconnected slots (in prev but not in cur) produce a slot with
33
+ * standardMapping=false and all readpoints empty.
34
+ * - Null entries in the browser getGamepads() array are skipped by the
35
+ * caller before calling this function.
36
+ *
37
+ * @param remapLookup - Optional acquisition-layer normalizer. Given a
38
+ * browser Gamepad.id, returns SDL mapping tokens or null. Only consulted
39
+ * for non-standard-mapping gamepads (D-1 option A).
40
+ */
41
+ export declare function diffGamepadFrame(prev: ReadonlyMap<number, GamepadSlotSample>, curGamepads: readonly RawGamepadStub[], remapLookup?: (gamepadId: string) => MappingTokens | null): GamepadSlotSample[];
42
+ //# sourceMappingURL=gamepad-frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gamepad-frame.d.ts","sourceRoot":"","sources":["../src/gamepad-frame.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IACnF,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAyHD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC5C,WAAW,EAAE,SAAS,cAAc,EAAE,EACtC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,aAAa,GAAG,IAAI,GACxD,iBAAiB,EAAE,CAwBrB"}
@@ -0,0 +1,145 @@
1
+ import type { PointerPhaseEvent, PointerType } from './input-snapshot';
2
+ export declare const LONG_PRESS_DURATION_MS = 500;
3
+ export declare const LONG_PRESS_SLOP = 10;
4
+ export declare const DOUBLE_TAP_INTERVAL_MS = 350;
5
+ export declare const DOUBLE_TAP_DISTANCE = 10;
6
+ export declare const SWIPE_VELOCITY_THRESHOLD = 0.5;
7
+ export declare const SWIPE_WINDOW_MS = 100;
8
+ /** Swipe direction taken from the dominant displacement axis (D-10). */
9
+ declare const SWIPE_DIRECTIONS: readonly ["left", "right", "up", "down"];
10
+ export type SwipeDirection = (typeof SWIPE_DIRECTIONS)[number];
11
+ /**
12
+ * Continuous gesture values read each frame via `snap.gesture` (D-4).
13
+ * Identity when no gesture is active or after a cancel/reset (AC-12):
14
+ * pinchScale = 1.0, rotationAngle = 0.
15
+ */
16
+ export interface GestureState {
17
+ readonly pinchScale: number;
18
+ readonly rotationAngle: number;
19
+ }
20
+ /**
21
+ * Closed discriminant union of gesture lifecycle + instantaneous events
22
+ * (AC-13). One-frame lifecycle: an event appears in exactly one frame's
23
+ * `gestureEvents`. Every member carries `pointerType` (narrowed union,
24
+ * AC-19) so consumers can exhaustively switch on gesture kind AND device
25
+ * type without a default branch.
26
+ */
27
+ export type GestureEvent = {
28
+ readonly kind: 'pinch-begin';
29
+ readonly pointerIds: readonly [number, number];
30
+ readonly pointerType: PointerType;
31
+ readonly timestamp: number;
32
+ } | {
33
+ readonly kind: 'pinch-end';
34
+ readonly pointerIds: readonly [number, number];
35
+ readonly pointerType: PointerType;
36
+ readonly timestamp: number;
37
+ } | {
38
+ readonly kind: 'pinch-cancel';
39
+ readonly pointerIds: readonly [number, number];
40
+ readonly pointerType: PointerType;
41
+ readonly timestamp: number;
42
+ } | {
43
+ readonly kind: 'rotate-begin';
44
+ readonly pointerIds: readonly [number, number];
45
+ readonly pointerType: PointerType;
46
+ readonly timestamp: number;
47
+ } | {
48
+ readonly kind: 'rotate-end';
49
+ readonly pointerIds: readonly [number, number];
50
+ readonly pointerType: PointerType;
51
+ readonly timestamp: number;
52
+ } | {
53
+ readonly kind: 'rotate-cancel';
54
+ readonly pointerIds: readonly [number, number];
55
+ readonly pointerType: PointerType;
56
+ readonly timestamp: number;
57
+ } | {
58
+ readonly kind: 'swipe';
59
+ readonly pointerId: number;
60
+ readonly direction: SwipeDirection;
61
+ readonly pointerType: PointerType;
62
+ readonly timestamp: number;
63
+ } | {
64
+ readonly kind: 'long-press';
65
+ readonly pointerId: number;
66
+ readonly x: number;
67
+ readonly y: number;
68
+ readonly pointerType: PointerType;
69
+ readonly timestamp: number;
70
+ } | {
71
+ readonly kind: 'double-tap';
72
+ readonly pointerId: number;
73
+ readonly x: number;
74
+ readonly y: number;
75
+ readonly pointerType: PointerType;
76
+ readonly timestamp: number;
77
+ };
78
+ /** Live position + device type for a pointer (subset of the backend pointerMap entry). */
79
+ export interface RecognizerPointer {
80
+ readonly x: number;
81
+ readonly y: number;
82
+ readonly pointerType: PointerType;
83
+ }
84
+ /** Locked dual-finger tracker for pinch + rotate (D-11). */
85
+ export interface PinchRotateTracker {
86
+ readonly pointerA: number;
87
+ readonly pointerB: number;
88
+ readonly initialDistance: number;
89
+ /** Previous frame's raw atan2 angle; rotation accumulates frame deltas (D-11). */
90
+ readonly lastAngle: number;
91
+ readonly pointerType: PointerType;
92
+ }
93
+ /** Per-pointer long-press arming state (D-10). */
94
+ export interface LongPressTracker {
95
+ readonly downTime: number;
96
+ readonly x: number;
97
+ readonly y: number;
98
+ readonly pointerType: PointerType;
99
+ readonly armed: boolean;
100
+ readonly fired: boolean;
101
+ }
102
+ /** Timestamped position sample for swipe velocity (D-10, 100ms window). */
103
+ export interface SwipeSample {
104
+ readonly t: number;
105
+ readonly x: number;
106
+ readonly y: number;
107
+ }
108
+ /** Last recorded tap for double-tap window matching (D-10). */
109
+ export interface TapRecord {
110
+ readonly time: number;
111
+ readonly x: number;
112
+ readonly y: number;
113
+ }
114
+ /**
115
+ * The single legal cross-frame gesture state (C-3). Immutable: each
116
+ * `processGestureFrame` returns a fresh copy; the producer stores it for
117
+ * the next frame. `gesture` is the current continuous value read by
118
+ * `snap.gesture` -- live during pinch, frozen after a 2->1 end, identity
119
+ * after cancel/reset or when no gesture ever activated.
120
+ */
121
+ export interface RecognizerState {
122
+ readonly pinch: PinchRotateTracker | null;
123
+ readonly gesture: GestureState;
124
+ readonly longPresses: ReadonlyMap<number, LongPressTracker>;
125
+ readonly lastTap: TapRecord | null;
126
+ readonly swipeHistory: ReadonlyMap<number, readonly SwipeSample[]>;
127
+ }
128
+ /** Result of processing one frame of phase events (D-4 dual channel). */
129
+ export interface GestureFrameResult {
130
+ readonly newState: RecognizerState;
131
+ readonly gestureState: GestureState;
132
+ readonly gestureEvents: readonly GestureEvent[];
133
+ }
134
+ /** Identity continuous value (AC-12): no scale, no rotation. */
135
+ export declare const IDENTITY_GESTURE: GestureState;
136
+ /** Fresh recognizer state with no active gestures. */
137
+ export declare function createRecognizerState(): RecognizerState;
138
+ /**
139
+ * Process one frame of pointer phase events, advancing all timers off
140
+ * `now`, and return the new state + continuous values + one-frame event
141
+ * list (D-4). Never mutates its inputs.
142
+ */
143
+ export declare function processGestureFrame(phaseQueue: readonly PointerPhaseEvent[], pointerMap: ReadonlyMap<number, RecognizerPointer>, prevState: RecognizerState, now: number): GestureFrameResult;
144
+ export {};
145
+ //# sourceMappingURL=gesture-recognizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gesture-recognizer.d.ts","sourceRoot":"","sources":["../src/gesture-recognizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type { ActionBinding, ActionConfig, ActionState, GetVectorOptions, } from './action-state';
2
+ export { deriveActionStates, getAxis, getVector, INPUT_MAP_KEY, } from './action-state';
3
+ export { attachBrowserInputBackend, type BrowserInputBackendOptions, type PointerLockProvider, } from './browser-backend';
4
+ export { type CanvasInputBoundary, createCanvasInputBoundary, } from './canvas-input-boundary';
5
+ export { type CompositeBackendOptions, type CompositeInputBackend, makeCompositeBackend, } from './composite-backend';
6
+ export { FRAME_START_SCAN_SYSTEM_NAME, INPUT_BACKEND_KEY, InputFrameStartScan, InputSet, } from './frame-start-scan-system';
7
+ export type { GestureEvent, GestureState, SwipeDirection, } from './gesture-recognizer';
8
+ export { DOUBLE_TAP_DISTANCE, DOUBLE_TAP_INTERVAL_MS, IDENTITY_GESTURE, LONG_PRESS_DURATION_MS, LONG_PRESS_SLOP, SWIPE_VELOCITY_THRESHOLD, SWIPE_WINDOW_MS, } from './gesture-recognizer';
9
+ export type { Capabilities, GamepadAxisIndex, GamepadButtonIndex, GamepadSlotSample, InputBackend, InputBackendSample, InputSnapshot, MouseButtonIndex, PointerPhaseEvent, PointerSample, PointerType, VirtualAxisSample, VirtualJoystickConfig, } from './input-snapshot';
10
+ export { createInputSnapshot, INPUT_SNAPSHOT_RESOURCE_KEY, snapshotFromSample, } from './input-snapshot';
11
+ export { inputBackendPlugin, ownedInputBackendPlugin } from './plugin-service';
12
+ export { createUiInputResetBoundary, isUiOwnedEvent, resolveUiOwnership, type UiInputResetBoundary, type UiInputResetBoundaryOptions, type UiOwnershipResult, } from './ui-ownership';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0BA,YAAY,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,OAAO,EACP,SAAS,EACT,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,mBAAmB,EACxB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,4BAA4B,EAC5B,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,GACT,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,wBAAwB,EACxB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EACL,0BAA0B,EAC1B,cAAc,EACd,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAC"}