@glassly/engine 0.3.3-dev.621 → 0.3.3-dev.623
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.
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +2 -0
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +25 -9
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +4 -2
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +39 -1
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GestureDotsService.ts +20 -7
- package/src/services/GlassesTouchInput.ts +4 -2
- package/src/services/LocalMiniappRuntime.ts +4 -2
- package/src/services/effectiveCapabilities.ts +49 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAoBA,OAAO,EAEL,WAAW,EAGX,KAAK,YAAY,EAClB,MAAM,UAAU,CAAA;AAQjB,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAI3D;AAMD,iGAAiG;AACjG,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAEzE"}
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
* lifted alongside the canvas, bounded only to keep frames sane. Firmware
|
|
12
12
|
* that draws vector shapes keeps 128 scene slots, so shape-heavy frames
|
|
13
13
|
* (a game board, a chart) get a larger shared text/shape budget.
|
|
14
|
+
*
|
|
15
|
+
* A connected controller that swipes in two dimensions (the Meta Neural Band)
|
|
16
|
+
* adds its surface to `button.buttons` as a `swipe2d` entry, so a miniapp can
|
|
17
|
+
* tell four-way swipes from the temples' and the R1 ring's up/down.
|
|
14
18
|
*/
|
|
15
19
|
import { useGlassesStore } from "../stores/glasses";
|
|
16
20
|
import { SETTINGS, useSettingsStore } from "../stores/settings";
|
|
17
|
-
import { ALL_DISPLAY_SHAPES, getModelCapabilities } from "../types";
|
|
21
|
+
import { ALL_DISPLAY_SHAPES, getModelCapabilities, } from "../types";
|
|
18
22
|
import { resolveNativeNavigation } from "./nativeNavigationGate";
|
|
23
|
+
import { input2dFrom } from "./settingsBridge";
|
|
19
24
|
const CFW_MAX_TEXT_ELEMENTS = 24;
|
|
20
25
|
const CFW_MAX_IMAGE_ELEMENTS = 16;
|
|
21
26
|
const CFW_VECTOR_MAX_ELEMENTS = 120;
|
|
@@ -33,6 +38,9 @@ export function runtimeVolumeGate() {
|
|
|
33
38
|
return useGlassesStore.getState().volumeGate === true;
|
|
34
39
|
}
|
|
35
40
|
export function getEffectiveCapabilities(model) {
|
|
41
|
+
return withControllerInput(glassesCapabilities(model));
|
|
42
|
+
}
|
|
43
|
+
function glassesCapabilities(model) {
|
|
36
44
|
const base = getModelCapabilities(model);
|
|
37
45
|
// Only models with a Glassly CFW publish a canvas or a volume gate; never
|
|
38
46
|
// let a stale value reshape another model.
|
|
@@ -61,6 +69,36 @@ export function getEffectiveCapabilities(model) {
|
|
|
61
69
|
},
|
|
62
70
|
};
|
|
63
71
|
}
|
|
72
|
+
/** The Neural Band's thumb swipes, pinches and dial. */
|
|
73
|
+
const NEURAL_BAND_SURFACE = {
|
|
74
|
+
type: "swipe2d",
|
|
75
|
+
events: [
|
|
76
|
+
"TAP",
|
|
77
|
+
"DOUBLE_TAP",
|
|
78
|
+
"PRESS_HOLD",
|
|
79
|
+
"SWIPE_UP",
|
|
80
|
+
"SWIPE_DOWN",
|
|
81
|
+
"SWIPE_LEFT",
|
|
82
|
+
"SWIPE_RIGHT",
|
|
83
|
+
"DIAL_UP",
|
|
84
|
+
"DIAL_DOWN",
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
function withControllerInput(caps) {
|
|
88
|
+
const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key);
|
|
89
|
+
if (!input2dFrom(useGlassesStore.getState(), controller))
|
|
90
|
+
return caps;
|
|
91
|
+
const buttons = caps.button?.buttons ?? [];
|
|
92
|
+
return {
|
|
93
|
+
...caps,
|
|
94
|
+
hasButton: true,
|
|
95
|
+
button: {
|
|
96
|
+
...caps.button,
|
|
97
|
+
count: (caps.button?.count ?? buttons.length) + 1,
|
|
98
|
+
buttons: [...buttons, NEURAL_BAND_SURFACE],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
64
102
|
function withVolumeGate(base) {
|
|
65
103
|
return { ...base, microphone: { ...(base.microphone ?? {}), volumeGate: true } };
|
|
66
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Effective device capabilities — the static model profile plus what the\n * connected firmware reports at runtime. The runtime override is the scene\n * canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into\n * the 640×480 panel, so the drawable canvas grows past stock's 576×288; on\n * the RayNeo iO it opens a 540×180 page that stock firmware has no\n * equivalent of. Any model whose profile declares `hasCustomFirmware` reads it.\n * Native publishes the canvas into the glasses status (0 = none). Because\n * that path is phone-side rasterization, the stock firmware's container\n * pools (6 text / 4 image) don't bound a CFW frame either — element caps are\n * lifted alongside the canvas, bounded only to keep frames sane. Firmware\n * that draws vector shapes keeps 128 scene slots, so shape-heavy frames\n * (a game board, a chart) get a larger shared text/shape budget.\n *\n * A connected controller that swipes in two dimensions (the Meta Neural Band)\n * adds its surface to `button.buttons` as a `swipe2d` entry, so a miniapp can\n * tell four-way swipes from the temples' and the R1 ring's up/down.\n */\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {\n ALL_DISPLAY_SHAPES,\n DeviceTypes,\n getModelCapabilities,\n type ButtonCapabilities,\n type Capabilities,\n} from \"../types\"\nimport {resolveNativeNavigation} from \"./nativeNavigationGate\"\nimport {input2dFrom} from \"./settingsBridge\"\n\nconst CFW_MAX_TEXT_ELEMENTS = 24\nconst CFW_MAX_IMAGE_ELEMENTS = 16\nconst CFW_VECTOR_MAX_ELEMENTS = 120\n\nexport interface SceneCanvasSize {\n width: number\n height: number\n}\n\nexport function runtimeSceneCanvas(): SceneCanvasSize | null {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (!sceneCanvasWidth || !sceneCanvasHeight || sceneCanvasWidth <= 0 || sceneCanvasHeight <= 0) return null\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n}\n\nfunction forceNativeNavigationOnCfw(): boolean {\n return useSettingsStore.getState().getSetting(SETTINGS.cfw_native_navigation.key) === true\n}\n\n/** The connected firmware gates microphone streaming on volume (G2 Glassly CFW revision 40+). */\nexport function runtimeVolumeGate(): boolean {\n return useGlassesStore.getState().volumeGate === true\n}\n\nexport function getEffectiveCapabilities(model: DeviceTypes): Capabilities {\n return withControllerInput(glassesCapabilities(model))\n}\n\nfunction glassesCapabilities(model: DeviceTypes): Capabilities {\n const base = getModelCapabilities(model)\n // Only models with a Glassly CFW publish a canvas or a volume gate; never\n // let a stale value reshape another model.\n const canvas = base.hasCustomFirmware ? runtimeSceneCanvas() : null\n const volumeGate = base.hasCustomFirmware && base.hasMicrophone && runtimeVolumeGate()\n if (!canvas) return volumeGate ? withVolumeGate(base) : base\n if (!base.display) return volumeGate ? withVolumeGate(base) : base\n // Glassly CFW supports the complete vector contract, including SVG and rotation.\n // Upstream g2flash publishes a raster canvas with sceneVector=false.\n const vector = useGlassesStore.getState().sceneVector === true\n return {\n ...(volumeGate ? withVolumeGate(base) : base),\n hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),\n display: {\n ...base.display,\n width: canvas.width,\n height: canvas.height,\n maxTextElements: Math.max(\n base.display.maxTextElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_TEXT_ELEMENTS,\n ),\n // Vector firmware draws an image as one cached slot, the same as a shape.\n maxImageElements: Math.max(\n base.display.maxImageElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,\n ),\n ...(vector\n ? {shapes: [...ALL_DISPLAY_SHAPES, \"svg\" as const], animation: true, rotation: true, depth: true}\n : {}),\n },\n }\n}\n\ntype InputSurface = NonNullable<ButtonCapabilities[\"buttons\"]>[number]\n\n/** The Neural Band's thumb swipes, pinches and dial. */\nconst NEURAL_BAND_SURFACE: InputSurface = {\n type: \"swipe2d\",\n events: [\n \"TAP\",\n \"DOUBLE_TAP\",\n \"PRESS_HOLD\",\n \"SWIPE_UP\",\n \"SWIPE_DOWN\",\n \"SWIPE_LEFT\",\n \"SWIPE_RIGHT\",\n \"DIAL_UP\",\n \"DIAL_DOWN\",\n ],\n}\n\nfunction withControllerInput(caps: Capabilities): Capabilities {\n const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key)\n if (!input2dFrom(useGlassesStore.getState(), controller)) return caps\n const buttons = caps.button?.buttons ?? []\n return {\n ...caps,\n hasButton: true,\n button: {\n ...caps.button,\n count: (caps.button?.count ?? buttons.length) + 1,\n buttons: [...buttons, NEURAL_BAND_SURFACE],\n },\n }\n}\n\nfunction withVolumeGate(base: Capabilities): Capabilities {\n return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.3-dev.
|
|
3
|
+
"version": "0.3.3-dev.623",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.3-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.3-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.3-dev.
|
|
103
|
-
"@glassly/crust": "0.3.3-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.3-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.3-dev.623",
|
|
101
|
+
"@glassly/cloud-client": "0.3.3-dev.623",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.3-dev.623",
|
|
103
|
+
"@glassly/crust": "0.3.3-dev.623",
|
|
104
|
+
"@glassly/miniapp": "0.3.3-dev.623",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.1.1",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.3-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.3-dev.623"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@glassly/llm-providers": "workspace:*",
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.3",
|
|
15
|
-
"releaseIdentity": "0.3.3-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.3-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.3-dev.623",
|
|
16
|
+
"releaseSetId": "glassly-0.3.3-dev.623",
|
|
17
|
+
"sourceCommit": "6eb106fb7c84c6b17cd3d46557f75bb81a23f191",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* glasses when a touch gesture arrives, while nothing else is on the display:
|
|
4
4
|
* a filled circle for a single tap, two for a double tap, an arrow for a swipe
|
|
5
5
|
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
|
-
*
|
|
6
|
+
* Controllers that report press edges (touch_down … tap_end) show the press
|
|
7
|
+
* itself instead: a dot while pressed, the bullseye once held, cleared at
|
|
8
|
+
* release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
9
|
* the CFW dashboard service; it renders through
|
|
8
10
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
9
11
|
* G2 included), and is gated by the gesture_dots debug setting.
|
|
@@ -24,6 +26,8 @@ import sceneRenderer from "./SceneRenderer"
|
|
|
24
26
|
const DOTS_APP_ID = "system.gesture-dots"
|
|
25
27
|
/** How long a gesture stays on the indicator. */
|
|
26
28
|
const GESTURE_FLASH_MS = 700
|
|
29
|
+
/** A press-edge device's tap and hold reports trail its last edge by its double-tap window. */
|
|
30
|
+
const EDGE_REPORT_GRACE_MS = 1000
|
|
27
31
|
const MARGIN_X = 16
|
|
28
32
|
const MARGIN_TOP = 10
|
|
29
33
|
/** Gestures that flash a glyph. */
|
|
@@ -45,6 +49,8 @@ class GestureDotsService {
|
|
|
45
49
|
private flashTimer: number | null = null
|
|
46
50
|
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
47
51
|
private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()
|
|
52
|
+
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
53
|
+
private lastEdgeAt = new Map<number, number>()
|
|
48
54
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
49
55
|
private renderedGlyph: GestureDotGlyph | null = null
|
|
50
56
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -76,6 +82,7 @@ class GestureDotsService {
|
|
|
76
82
|
for (const unsub of this.unsubs) unsub()
|
|
77
83
|
this.unsubs = []
|
|
78
84
|
this.relinquish()
|
|
85
|
+
this.lastEdgeAt.clear()
|
|
79
86
|
this.started = false
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -124,9 +131,12 @@ class GestureDotsService {
|
|
|
124
131
|
const finger = fingerIndex ?? 0
|
|
125
132
|
const press = this.presses.get(finger)
|
|
126
133
|
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
134
|
+
if (gestureName === "touch_down") this.lastEdgeAt.set(finger, Date.now())
|
|
127
135
|
if (press) {
|
|
128
|
-
// Both reports describe one press
|
|
136
|
+
// Both reports describe one press: after touch_down, tap_start means it is held.
|
|
129
137
|
if (gestureName === "touch_down") press.raw = true
|
|
138
|
+
else if (press.raw) press.glyph = "hold"
|
|
139
|
+
this.render()
|
|
130
140
|
return
|
|
131
141
|
}
|
|
132
142
|
this.clearFlash()
|
|
@@ -137,21 +147,24 @@ class GestureDotsService {
|
|
|
137
147
|
if (gestureName === "tap_end") {
|
|
138
148
|
if (!press) return
|
|
139
149
|
this.presses.delete(finger)
|
|
140
|
-
|
|
141
|
-
// Keep the glyph lit across that gap so one pinch never flashes twice.
|
|
142
|
-
if (press.raw && this.presses.size === 0) this.startFlash(press.glyph)
|
|
150
|
+
if (press.raw) this.lastEdgeAt.set(finger, Date.now())
|
|
143
151
|
this.render()
|
|
144
152
|
return
|
|
145
153
|
}
|
|
146
154
|
const shape = FLASH_GLYPHS[gestureName]
|
|
147
155
|
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
|
|
148
|
-
|
|
156
|
+
// The press already showed its own tap and hold; the device's report of
|
|
157
|
+
// them follows after its double-tap window, often after release.
|
|
158
|
+
const shownByPress = gestureName === "single_tap" || gestureName === "long_press"
|
|
159
|
+
if (press?.raw && (!glyph || shownByPress)) {
|
|
149
160
|
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
150
161
|
// stays visible until tap_end instead of expiring halfway through it.
|
|
151
|
-
if (
|
|
162
|
+
if (gestureName === "long_press") press.glyph = "hold"
|
|
152
163
|
this.render()
|
|
153
164
|
return
|
|
154
165
|
}
|
|
166
|
+
const lastEdgeAt = this.lastEdgeAt.get(finger)
|
|
167
|
+
if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return
|
|
155
168
|
// Completed taps/swipes become one continuous pulse, even if the device
|
|
156
169
|
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
157
170
|
this.presses.delete(finger)
|
|
@@ -7,6 +7,7 @@ import {useGlassesStore} from "../stores/glasses"
|
|
|
7
7
|
import {BgTimer} from "../utils/timers"
|
|
8
8
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
9
9
|
import gestureDotsService from "./GestureDotsService"
|
|
10
|
+
import {isGlassesConnected} from "./GlassesReadiness"
|
|
10
11
|
import localDisplayManager from "./LocalDisplayManager"
|
|
11
12
|
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
12
13
|
import {MiniappExitGesture} from "./MiniappExitGesture"
|
|
@@ -92,8 +93,9 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
92
93
|
// Nobody claimed a double tap: the glasses' own dashboard. On eligible
|
|
93
94
|
// Glassly CFW glasses the dashboard miniapp claims it while it runs
|
|
94
95
|
// (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The
|
|
95
|
-
// modal nav UI owns the screen; showDashboard would fight it.
|
|
96
|
-
|
|
96
|
+
// modal nav UI owns the screen; showDashboard would fight it. Without
|
|
97
|
+
// glasses there is no dashboard to open, and the mirror keeps its frame.
|
|
98
|
+
if (osDoubleTap && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
97
99
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
98
100
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
99
101
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|
|
@@ -5330,12 +5330,14 @@ class LocalMiniappRuntime {
|
|
|
5330
5330
|
if (this.capabilitiesWired) return
|
|
5331
5331
|
this.capabilitiesWired = true
|
|
5332
5332
|
const broadcast = () => this.broadcastCapabilities()
|
|
5333
|
-
// Connection, model and firmware also decide the dashboard view's eligibility
|
|
5333
|
+
// Connection, model and firmware also decide the dashboard view's eligibility;
|
|
5334
|
+
// a connected Neural Band adds its swipe2d surface to `button`.
|
|
5334
5335
|
useGlassesStore.subscribe(
|
|
5335
5336
|
(s) =>
|
|
5336
|
-
`${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}`,
|
|
5337
|
+
`${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${s.controllerConnected}`,
|
|
5337
5338
|
broadcast,
|
|
5338
5339
|
)
|
|
5340
|
+
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), broadcast)
|
|
5339
5341
|
// The debug knob flips hasNativeNavigation on custom firmware.
|
|
5340
5342
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
|
|
5341
5343
|
// The dashboard master switch flips the dashboard view.
|
|
@@ -11,11 +11,22 @@
|
|
|
11
11
|
* lifted alongside the canvas, bounded only to keep frames sane. Firmware
|
|
12
12
|
* that draws vector shapes keeps 128 scene slots, so shape-heavy frames
|
|
13
13
|
* (a game board, a chart) get a larger shared text/shape budget.
|
|
14
|
+
*
|
|
15
|
+
* A connected controller that swipes in two dimensions (the Meta Neural Band)
|
|
16
|
+
* adds its surface to `button.buttons` as a `swipe2d` entry, so a miniapp can
|
|
17
|
+
* tell four-way swipes from the temples' and the R1 ring's up/down.
|
|
14
18
|
*/
|
|
15
19
|
import {useGlassesStore} from "../stores/glasses"
|
|
16
20
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
17
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ALL_DISPLAY_SHAPES,
|
|
23
|
+
DeviceTypes,
|
|
24
|
+
getModelCapabilities,
|
|
25
|
+
type ButtonCapabilities,
|
|
26
|
+
type Capabilities,
|
|
27
|
+
} from "../types"
|
|
18
28
|
import {resolveNativeNavigation} from "./nativeNavigationGate"
|
|
29
|
+
import {input2dFrom} from "./settingsBridge"
|
|
19
30
|
|
|
20
31
|
const CFW_MAX_TEXT_ELEMENTS = 24
|
|
21
32
|
const CFW_MAX_IMAGE_ELEMENTS = 16
|
|
@@ -42,6 +53,10 @@ export function runtimeVolumeGate(): boolean {
|
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
|
|
56
|
+
return withControllerInput(glassesCapabilities(model))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function glassesCapabilities(model: DeviceTypes): Capabilities {
|
|
45
60
|
const base = getModelCapabilities(model)
|
|
46
61
|
// Only models with a Glassly CFW publish a canvas or a volume gate; never
|
|
47
62
|
// let a stale value reshape another model.
|
|
@@ -75,6 +90,39 @@ export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
|
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
type InputSurface = NonNullable<ButtonCapabilities["buttons"]>[number]
|
|
94
|
+
|
|
95
|
+
/** The Neural Band's thumb swipes, pinches and dial. */
|
|
96
|
+
const NEURAL_BAND_SURFACE: InputSurface = {
|
|
97
|
+
type: "swipe2d",
|
|
98
|
+
events: [
|
|
99
|
+
"TAP",
|
|
100
|
+
"DOUBLE_TAP",
|
|
101
|
+
"PRESS_HOLD",
|
|
102
|
+
"SWIPE_UP",
|
|
103
|
+
"SWIPE_DOWN",
|
|
104
|
+
"SWIPE_LEFT",
|
|
105
|
+
"SWIPE_RIGHT",
|
|
106
|
+
"DIAL_UP",
|
|
107
|
+
"DIAL_DOWN",
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function withControllerInput(caps: Capabilities): Capabilities {
|
|
112
|
+
const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key)
|
|
113
|
+
if (!input2dFrom(useGlassesStore.getState(), controller)) return caps
|
|
114
|
+
const buttons = caps.button?.buttons ?? []
|
|
115
|
+
return {
|
|
116
|
+
...caps,
|
|
117
|
+
hasButton: true,
|
|
118
|
+
button: {
|
|
119
|
+
...caps.button,
|
|
120
|
+
count: (caps.button?.count ?? buttons.length) + 1,
|
|
121
|
+
buttons: [...buttons, NEURAL_BAND_SURFACE],
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
78
126
|
function withVolumeGate(base: Capabilities): Capabilities {
|
|
79
127
|
return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}
|
|
80
128
|
}
|