@glassly/bluetooth-sdk 0.1.0-dev.112 → 0.1.0-dev.113
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/android/src/main/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +3 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +79 -21
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +2 -2
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +37 -3
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/sgcs/G2.swift +7 -1
- package/ios/Source/sgcs/G2CfwRenderer.swift +5 -3
- package/ios/Source/sgcs/G2CfwScene.swift +67 -19
- package/ios/Source/sgcs/G2Flasher.swift +2 -2
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +46 -2
- package/package.json +1 -1
- package/src/generated/releaseMetadata.ts +3 -3
|
Binary file
|
|
@@ -3,9 +3,9 @@ package com.glassly.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "0.1.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "0.1.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "glassly-0.1.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "0.1.0-dev.113"
|
|
7
|
+
const val RELEASE_SET_ID: String = "glassly-0.1.0-dev.113"
|
|
8
|
+
const val SOURCE_COMMIT: String = "e63da43b467758373b960b0ee9167ef29938a11c"
|
|
9
9
|
const val OTA_MANIFEST_URL: String = ""
|
|
10
10
|
const val OTA_MANIFEST_SHA256: String = ""
|
|
11
11
|
}
|
|
@@ -1578,6 +1578,10 @@ class G2 : SGCManager() {
|
|
|
1578
1578
|
supportsCfwSceneRendering &&
|
|
1579
1579
|
listOf("scene17", "shapes16", "anim18", "texcache12", "teximg13", "font15").all { it in cfwFeatures }
|
|
1580
1580
|
|
|
1581
|
+
/** TEXT_INLINE records: default text drawn by the glasses' font, no cache upload. */
|
|
1582
|
+
private val supportsCfwInlineText: Boolean
|
|
1583
|
+
get() = "inlinetext17" in cfwFeatures
|
|
1584
|
+
|
|
1581
1585
|
/** The on-glass slot table must be re-SET on the next vector frame. */
|
|
1582
1586
|
private var cfwSceneInvalid = true
|
|
1583
1587
|
|
|
@@ -3104,9 +3108,10 @@ class G2 : SGCManager() {
|
|
|
3104
3108
|
}
|
|
3105
3109
|
if (cfwLastBoxes != boxes) cfwSceneInvalid = true
|
|
3106
3110
|
val invalidate = cfwSceneInvalid
|
|
3111
|
+
val inlineText = supportsCfwInlineText
|
|
3107
3112
|
val encoded =
|
|
3108
3113
|
kotlinx.coroutines.withContext(Dispatchers.Default) {
|
|
3109
|
-
G2CfwRenderer.encodeVector(frame, boxes, cfwCacheGeneration, invalidate)
|
|
3114
|
+
G2CfwRenderer.encodeVector(frame, boxes, cfwCacheGeneration, invalidate, inlineText)
|
|
3110
3115
|
}
|
|
3111
3116
|
if (encoded == null) {
|
|
3112
3117
|
Bridge.log("G2: vector scene does not fit; using the raster path for this frame")
|
|
@@ -87,6 +87,7 @@ internal object G2CfwRenderer {
|
|
|
87
87
|
boxes: LensBoxes,
|
|
88
88
|
cacheGeneration: Int,
|
|
89
89
|
invalidate: Boolean,
|
|
90
|
+
inlineText: Boolean,
|
|
90
91
|
): G2CfwScene.Encoded? {
|
|
91
92
|
if (textureCache.generation != cacheGeneration || textureCache.exhausted) {
|
|
92
93
|
textureCache.reset(cacheGeneration)
|
|
@@ -103,11 +104,11 @@ internal object G2CfwRenderer {
|
|
|
103
104
|
h,
|
|
104
105
|
)
|
|
105
106
|
}
|
|
106
|
-
scene.encode(frame, boxes.leftX, boxes.leftY, textureCache, rasterizeOne)?.let { return it }
|
|
107
|
+
scene.encode(frame, boxes.leftX, boxes.leftY, textureCache, rasterizeOne, inlineText)?.let { return it }
|
|
107
108
|
// Out of texture cache mid-frame: start a fresh cache and repack once.
|
|
108
109
|
textureCache.reset(cacheGeneration)
|
|
109
110
|
scene.reset(cacheGeneration)
|
|
110
|
-
return scene.encode(frame, boxes.leftX, boxes.leftY, textureCache, rasterizeOne)
|
|
111
|
+
return scene.encode(frame, boxes.leftX, boxes.leftY, textureCache, rasterizeOne, inlineText)
|
|
111
112
|
}
|
|
112
113
|
private val glyphRasterMemo = mutableMapOf<String, ByteArray>()
|
|
113
114
|
|
|
@@ -6,13 +6,14 @@ import java.io.ByteArrayOutputStream
|
|
|
6
6
|
* Retained vector scene for the Glassly CFW (firmware modes 16-18).
|
|
7
7
|
*
|
|
8
8
|
* Every scene element becomes one or more firmware slots (paint order = slot
|
|
9
|
-
* order): vector shapes map 1:1
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* tiled to the 255 px slot limit.
|
|
13
|
-
* layout: unchanged slots cost nothing,
|
|
14
|
-
* transition become firmware TWEENs, and
|
|
15
|
-
* stay stable per element id; a reorder
|
|
9
|
+
* order): vector shapes map 1:1; default and builtin text becomes one inline
|
|
10
|
+
* TEXT slot per line drawn by the glasses' font (or a cache-backed TEXT slot on
|
|
11
|
+
* firmware without inline records); mono text and images are rasterized on
|
|
12
|
+
* the phone and drawn as cached IMAGE slots tiled to the 255 px slot limit.
|
|
13
|
+
* Frames diff against the last committed layout: unchanged slots cost nothing,
|
|
14
|
+
* geometry changes on an element with a transition become firmware TWEENs, and
|
|
15
|
+
* anything else is re-SET. Slot indices stay stable per element id; a reorder
|
|
16
|
+
* or an over-budget frame repacks.
|
|
16
17
|
*/
|
|
17
18
|
internal class G2CfwScene {
|
|
18
19
|
companion object {
|
|
@@ -20,7 +21,9 @@ internal class G2CfwScene {
|
|
|
20
21
|
const val RECORD_BYTES = 20
|
|
21
22
|
const val FRAME_PERIOD_MS = 33.0
|
|
22
23
|
const val TEXT_LINE_HEIGHT = 27
|
|
24
|
+
const val TEXT_INSET = 2
|
|
23
25
|
const val MAX_TEXT_BYTES = 64
|
|
26
|
+
const val MAX_INLINE_BYTES = 128
|
|
24
27
|
const val MAX_TILE_SIZE = 255
|
|
25
28
|
/** Mode-13/15 options: identity ramp, transparent source color 0. */
|
|
26
29
|
const val TEXT_OPTIONS = 0x1F
|
|
@@ -42,6 +45,7 @@ internal class G2CfwScene {
|
|
|
42
45
|
const val TYPE_IMAGE = 14
|
|
43
46
|
const val TYPE_TEXT = 15
|
|
44
47
|
const val TYPE_TEXT_CACHED = 16
|
|
48
|
+
const val TYPE_TEXT_INLINE = 17
|
|
45
49
|
|
|
46
50
|
/** Geometry parameters a TWEEN may interpolate, per type. Texture offsets and lengths never tween. */
|
|
47
51
|
fun tweenableMask(type: Int): Int =
|
|
@@ -52,6 +56,7 @@ internal class G2CfwScene {
|
|
|
52
56
|
TYPE_TRI, TYPE_TRI_FILL, TYPE_BEZIER2 -> 0x3F
|
|
53
57
|
TYPE_QUAD, TYPE_QUAD_FILL, TYPE_BEZIER3 -> 0xFF
|
|
54
58
|
TYPE_IMAGE, TYPE_TEXT, TYPE_TEXT_CACHED -> 0x03
|
|
59
|
+
TYPE_TEXT_INLINE -> 0x0F
|
|
55
60
|
else -> 0
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -71,7 +76,7 @@ internal class G2CfwScene {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/** UTF-8 bytes of a line without firmware control bytes (1..31), cut at a codepoint boundary. */
|
|
74
|
-
fun textBytes(line: String): ByteArray {
|
|
79
|
+
fun textBytes(line: String, limit: Int = MAX_TEXT_BYTES): ByteArray {
|
|
75
80
|
val out = ByteArrayOutputStream()
|
|
76
81
|
var i = 0
|
|
77
82
|
while (i < line.length) {
|
|
@@ -79,7 +84,7 @@ internal class G2CfwScene {
|
|
|
79
84
|
i += Character.charCount(cp)
|
|
80
85
|
if (cp < 32) continue
|
|
81
86
|
val encoded = String(Character.toChars(cp)).toByteArray(Charsets.UTF_8)
|
|
82
|
-
if (out.size() + encoded.size >
|
|
87
|
+
if (out.size() + encoded.size > limit) break
|
|
83
88
|
out.write(encoded)
|
|
84
89
|
}
|
|
85
90
|
return out.toByteArray()
|
|
@@ -96,7 +101,10 @@ internal class G2CfwScene {
|
|
|
96
101
|
out.write((value ushr 8) and 0xFF)
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* `[0][slot][type][flags=visible][color][width][p0..p7 LE]`, or for TEXT_INLINE
|
|
106
|
+
* `[0][slot][17][1][color][width][x][y][w][h][len][bytes]`.
|
|
107
|
+
*/
|
|
100
108
|
fun setRecord(slot: Slot, index: Int): ByteArray {
|
|
101
109
|
val out = ByteArrayOutputStream()
|
|
102
110
|
out.write(0)
|
|
@@ -105,7 +113,13 @@ internal class G2CfwScene {
|
|
|
105
113
|
out.write(1)
|
|
106
114
|
out.write(slot.color)
|
|
107
115
|
out.write(slot.width)
|
|
108
|
-
|
|
116
|
+
val inline = slot.type == TYPE_TEXT_INLINE
|
|
117
|
+
for (k in 0 until (if (inline) 4 else 8)) le(out, slot.params[k])
|
|
118
|
+
if (inline) {
|
|
119
|
+
val bytes = slot.text.take(MAX_INLINE_BYTES).toByteArray()
|
|
120
|
+
out.write(bytes.size)
|
|
121
|
+
out.write(bytes)
|
|
122
|
+
}
|
|
109
123
|
return out.toByteArray()
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -114,7 +128,7 @@ internal class G2CfwScene {
|
|
|
114
128
|
* change is not a pure geometry/stroke change of the same shape type.
|
|
115
129
|
*/
|
|
116
130
|
fun tweenRecord(old: Slot, new: Slot, index: Int, frames: Int, curve: IntArray): ByteArray? {
|
|
117
|
-
if (old.type != new.type) return null
|
|
131
|
+
if (old.type != new.type || !old.text.contentEquals(new.text)) return null
|
|
118
132
|
val tweenable = tweenableMask(new.type)
|
|
119
133
|
var mask = 0
|
|
120
134
|
val values = mutableListOf<Int>()
|
|
@@ -126,7 +140,9 @@ internal class G2CfwScene {
|
|
|
126
140
|
}
|
|
127
141
|
val isTexture = new.type >= TYPE_IMAGE
|
|
128
142
|
if (old.color != new.color) {
|
|
129
|
-
|
|
143
|
+
// Inline text may fade (low nibble) as long as its option bits stay put.
|
|
144
|
+
val inlineFade = new.type == TYPE_TEXT_INLINE && (old.color and 0xF0) == (new.color and 0xF0)
|
|
145
|
+
if (isTexture && !inlineFade) return null
|
|
130
146
|
mask = mask or 0x100
|
|
131
147
|
values.add(new.color)
|
|
132
148
|
}
|
|
@@ -147,15 +163,19 @@ internal class G2CfwScene {
|
|
|
147
163
|
}
|
|
148
164
|
}
|
|
149
165
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
166
|
+
/**
|
|
167
|
+
* One firmware slot: type, 4-bit color (or options byte), stroke width, eight
|
|
168
|
+
* int16 parameters, and the record's own bytes for TEXT_INLINE.
|
|
169
|
+
*/
|
|
170
|
+
class Slot(val type: Int, val color: Int, val width: Int, params: List<Int>, val text: ByteArray = ByteArray(0)) {
|
|
152
171
|
val params: IntArray = IntArray(8) { params.getOrElse(it) { 0 } }
|
|
153
172
|
|
|
154
173
|
override fun equals(other: Any?): Boolean =
|
|
155
174
|
other is Slot && other.type == type && other.color == color && other.width == width &&
|
|
156
|
-
other.params.contentEquals(params)
|
|
175
|
+
other.params.contentEquals(params) && other.text.contentEquals(text)
|
|
157
176
|
|
|
158
|
-
override fun hashCode(): Int =
|
|
177
|
+
override fun hashCode(): Int =
|
|
178
|
+
(((type * 31 + color) * 31 + width) * 31 + params.contentHashCode()) * 31 + text.contentHashCode()
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
class ElementState(val id: String, val contentHash: String, val slots: List<Slot>, val indices: List<Int>)
|
|
@@ -189,11 +209,15 @@ internal class G2CfwScene {
|
|
|
189
209
|
return Triple(clampCoord(element.x + element.w / 2 + ox), clampCoord(element.y + element.h / 2 + oy), r)
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Slots for one element, or null when the texture cache cannot hold its content.
|
|
214
|
+
* [inlineText] selects TEXT_INLINE records for default/builtin text.
|
|
215
|
+
*/
|
|
193
216
|
fun slotsFor(
|
|
194
217
|
element: SceneElement,
|
|
195
218
|
ox: Int,
|
|
196
219
|
oy: Int,
|
|
220
|
+
inlineText: Boolean,
|
|
197
221
|
cache: G2CfwTextureCache,
|
|
198
222
|
rasterize: (SceneElement, Int, Int) -> ByteArray?,
|
|
199
223
|
): List<Slot>? {
|
|
@@ -245,13 +269,46 @@ internal class G2CfwScene {
|
|
|
245
269
|
val params = (0 until n).flatMap { point(element, it, ox, oy).toList() }
|
|
246
270
|
listOf(Slot(if (n == 3) TYPE_BEZIER2 else TYPE_BEZIER3, color, stroke(element), params))
|
|
247
271
|
}
|
|
248
|
-
"text" ->
|
|
249
|
-
|
|
272
|
+
"text" ->
|
|
273
|
+
when {
|
|
274
|
+
element.font == "mono" -> rasterSlots(element, x, y, cache, rasterize)
|
|
275
|
+
inlineText -> inlineTextSlots(element, x, y)
|
|
276
|
+
element.font == "builtin" -> builtinTextSlots(element, x, y, cache)
|
|
277
|
+
else -> rasterSlots(element, x, y, cache, rasterize)
|
|
278
|
+
}
|
|
250
279
|
"image" -> rasterSlots(element, x, y, cache, rasterize)
|
|
251
280
|
else -> emptyList()
|
|
252
281
|
}
|
|
253
282
|
}
|
|
254
283
|
|
|
284
|
+
/** One TEXT_INLINE slot per line, drawn by the glasses' font at the phone-raster 2 px inset, clipped to the box. */
|
|
285
|
+
private fun inlineTextSlots(element: SceneElement, x: Int, y: Int): List<Slot> {
|
|
286
|
+
val out = mutableListOf<Slot>()
|
|
287
|
+
val w = element.w.coerceIn(0, 2047)
|
|
288
|
+
val h = element.h.coerceIn(0, 2047)
|
|
289
|
+
if (element.border > 0) {
|
|
290
|
+
out.add(Slot(TYPE_RECT, gray(element), stroke(element), listOf(x, y, w, h, element.radius.coerceIn(0, 2047))))
|
|
291
|
+
}
|
|
292
|
+
val options = 0x10 or gray(element)
|
|
293
|
+
val maxLines = maxOf(1, element.h / TEXT_LINE_HEIGHT)
|
|
294
|
+
val lines = (element.text ?: "").split("\n").take(maxLines)
|
|
295
|
+
for ((index, line) in lines.withIndex()) {
|
|
296
|
+
val bytes = textBytes(line, MAX_INLINE_BYTES)
|
|
297
|
+
if (bytes.isEmpty()) continue
|
|
298
|
+
val lineY = y + TEXT_INSET + index * TEXT_LINE_HEIGHT
|
|
299
|
+
val clipW = maxOf(0, element.w - TEXT_INSET).coerceAtMost(2047)
|
|
300
|
+
val clipH = maxOf(0, element.h - TEXT_INSET - index * TEXT_LINE_HEIGHT).coerceAtMost(2047)
|
|
301
|
+
out.add(
|
|
302
|
+
Slot(
|
|
303
|
+
TYPE_TEXT_INLINE, options, 0,
|
|
304
|
+
listOf(clampCoord(x + TEXT_INSET), clampCoord(lineY), clipW, clipH, bytes.size),
|
|
305
|
+
bytes,
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
return out
|
|
310
|
+
}
|
|
311
|
+
|
|
255
312
|
private fun builtinTextSlots(element: SceneElement, x: Int, y: Int, cache: G2CfwTextureCache): List<Slot>? {
|
|
256
313
|
val out = mutableListOf<Slot>()
|
|
257
314
|
if (element.border > 0) {
|
|
@@ -324,10 +381,11 @@ internal class G2CfwScene {
|
|
|
324
381
|
originY: Int,
|
|
325
382
|
cache: G2CfwTextureCache,
|
|
326
383
|
rasterize: (SceneElement, Int, Int) -> ByteArray?,
|
|
384
|
+
inlineText: Boolean = false,
|
|
327
385
|
): Encoded? {
|
|
328
386
|
val desired = mutableListOf<Desired>()
|
|
329
387
|
for (element in frame.elements) {
|
|
330
|
-
val slots = slotsFor(element, originX, originY, cache, rasterize) ?: return null
|
|
388
|
+
val slots = slotsFor(element, originX, originY, inlineText, cache, rasterize) ?: return null
|
|
331
389
|
desired.add(
|
|
332
390
|
Desired(element.id, element.contentHash, slots, transitionFrames(element.transitionMs), easingCurve(element.easing)),
|
|
333
391
|
)
|
|
@@ -50,8 +50,8 @@ internal object G2FirmwareCatalog {
|
|
|
50
50
|
displayName = "Glassly CFW",
|
|
51
51
|
version = "2.2.9.22 CFW",
|
|
52
52
|
fileName = "g2-glassly-cfw-2.2.9.22.evenota.bin",
|
|
53
|
-
sha256 = "
|
|
54
|
-
size =
|
|
53
|
+
sha256 = "560d4d94344573d11ba58690221c0ea7d5a5e8c8d29c12f158e81078484b815c",
|
|
54
|
+
size = 4_528_184,
|
|
55
55
|
),
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -35,8 +35,8 @@ class G2CfwSceneTest {
|
|
|
35
35
|
private fun frame(elements: List<SceneElement>, replay: Boolean = false) =
|
|
36
36
|
SceneFrame("app", 1, replay, elements, emptyList())
|
|
37
37
|
|
|
38
|
-
private fun encode(scene: G2CfwScene, cache: G2CfwTextureCache, elements: List<SceneElement
|
|
39
|
-
scene.encode(frame(elements), 32, 96, cache
|
|
38
|
+
private fun encode(scene: G2CfwScene, cache: G2CfwTextureCache, elements: List<SceneElement>, inlineText: Boolean = false) =
|
|
39
|
+
scene.encode(frame(elements), 32, 96, cache, { _, w, h -> ByteArray(w * h) { 0xFF.toByte() } }, inlineText)
|
|
40
40
|
|
|
41
41
|
private fun le(v: Int): List<Byte> = listOf((v and 0xFF).toByte(), ((v ushr 8) and 0xFF).toByte())
|
|
42
42
|
|
|
@@ -158,7 +158,7 @@ class G2CfwSceneTest {
|
|
|
158
158
|
val (scene, cache) = fresh()
|
|
159
159
|
assertNull(encode(scene, cache, (0 until 129).map { element("s$it", "circle", w = 11, h = 11) }))
|
|
160
160
|
val huge = element("big", "image", w = 2000, h = 2000, hash = "big")
|
|
161
|
-
val full = scene.encode(frame(listOf(huge)), 0, 0, cache
|
|
161
|
+
val full = scene.encode(frame(listOf(huge)), 0, 0, cache, { _, w, h -> ByteArray(w * h) { (it * 37).toByte() } })
|
|
162
162
|
assertNull(full)
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -172,4 +172,38 @@ class G2CfwSceneTest {
|
|
|
172
172
|
assertEquals(255, G2CfwScene.transitionFrames(60000))
|
|
173
173
|
assertArrayEquals(intArrayOf(107, 0, 148, 255), G2CfwScene.easingCurve("ease-in-out"))
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
fun inlineTextRecordsCarryTheirBytesAndAnimate() {
|
|
178
|
+
val (scene, cache) = fresh()
|
|
179
|
+
val mono = element("m", "text", y = 200, w = 100, h = 30, text = "grid", font = "mono", hash = "mono")
|
|
180
|
+
val first = requireNotNull(
|
|
181
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 10, w = 200, h = 60, text = "hi\nthere", color = 9), mono), true),
|
|
182
|
+
)
|
|
183
|
+
assertEquals(1, first.uploads.size)
|
|
184
|
+
assertEquals(listOf(listOf(0, 1), listOf(2)), scene.elements.map { it.indices })
|
|
185
|
+
val p = first.payload
|
|
186
|
+
assertEquals(listOf<Byte>(0, 0, 17, 1, 0x19, 0), bytes(p, 3, 9))
|
|
187
|
+
assertEquals(le(44) + le(108) + le(198) + le(58), bytes(p, 9, 17))
|
|
188
|
+
assertEquals(listOf<Byte>(2, 0x68, 0x69), bytes(p, 17, 20))
|
|
189
|
+
assertEquals(listOf<Byte>(0, 1, 17), bytes(p, 20, 23))
|
|
190
|
+
assertEquals(le(44) + le(135) + le(198) + le(31), bytes(p, 26, 34))
|
|
191
|
+
assertEquals("there".toByteArray().toList(), bytes(p, 35, 40))
|
|
192
|
+
assertEquals(0.toByte(), p[40])
|
|
193
|
+
assertEquals(14.toByte(), p[42])
|
|
194
|
+
|
|
195
|
+
val moved = requireNotNull(
|
|
196
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "hi\nthere", color = 9, transitionMs = 330), mono), true),
|
|
197
|
+
)
|
|
198
|
+
assertEquals(listOf<Byte>(5, 0, 0x02, 0x00, 10), bytes(moved.payload, 3, 8))
|
|
199
|
+
val retexted = requireNotNull(
|
|
200
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "ho\nthere", color = 9, transitionMs = 330), mono), true),
|
|
201
|
+
)
|
|
202
|
+
assertEquals(listOf<Byte>(0, 0, 17), bytes(retexted.payload, 3, 6))
|
|
203
|
+
val faded = requireNotNull(
|
|
204
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "ho\nthere", color = 3, transitionMs = 330), mono), true),
|
|
205
|
+
)
|
|
206
|
+
assertEquals(listOf<Byte>(5, 0, 0x00, 0x01, 10), bytes(faded.payload, 3, 8))
|
|
207
|
+
assertEquals(le(0x13), bytes(faded.payload, 12, 14))
|
|
208
|
+
}
|
|
175
209
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.1.0",
|
|
4
|
-
"releaseIdentity": "0.1.0-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.1.0-dev.113",
|
|
5
|
+
"releaseSetId": "glassly-0.1.0-dev.113",
|
|
6
|
+
"sourceCommit": "e63da43b467758373b960b0ee9167ef29938a11c",
|
|
7
7
|
"otaManifestUrl": null,
|
|
8
8
|
"otaManifestSha256": null
|
|
9
9
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.113\",\n \"releaseSetId\": \"glassly-0.1.0-dev.113\",\n \"sourceCommit\": \"e63da43b467758373b960b0ee9167ef29938a11c\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
|
|
|
11
11
|
static let voiceActivityDetectionEnabled = false
|
|
12
12
|
static let loudnessGateEnabled = true
|
|
13
13
|
private static let infoSdkVersionKey = "BluetoothSdkVersion"
|
|
14
|
-
private static let swiftPackageSdkVersion = "0.1.0-dev.
|
|
14
|
+
private static let swiftPackageSdkVersion = "0.1.0-dev.113"
|
|
15
15
|
private static let swiftPackageSdkVersionPlaceholder = "__GLASSLY" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
16
|
|
|
17
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "0.1.0"
|
|
6
|
-
static let releaseIdentity = "0.1.0-dev.
|
|
7
|
-
static let releaseSetId = "glassly-0.1.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
6
|
+
static let releaseIdentity = "0.1.0-dev.113"
|
|
7
|
+
static let releaseSetId = "glassly-0.1.0-dev.113"
|
|
8
|
+
static let sourceCommit = "e63da43b467758373b960b0ee9167ef29938a11c"
|
|
9
9
|
static let otaManifestUrl = ""
|
|
10
10
|
static let otaManifestSha256 = ""
|
|
11
11
|
}
|
|
Binary file
|
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -1810,6 +1810,11 @@ final class G2: NSObject, SGCManager {
|
|
|
1810
1810
|
.allSatisfy(cfwFeatures.contains)
|
|
1811
1811
|
}
|
|
1812
1812
|
|
|
1813
|
+
/// TEXT_INLINE records: default text drawn by the glasses' font, no cache upload.
|
|
1814
|
+
private var supportsCfwInlineText: Bool {
|
|
1815
|
+
cfwFeatures.contains("inlinetext17")
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1813
1818
|
/// The on-glass slot table must be re-SET on the next vector frame.
|
|
1814
1819
|
private var cfwSceneInvalid = true
|
|
1815
1820
|
|
|
@@ -3126,7 +3131,8 @@ final class G2: NSObject, SGCManager {
|
|
|
3126
3131
|
}
|
|
3127
3132
|
if cfwLastBoxes != boxes { cfwSceneInvalid = true }
|
|
3128
3133
|
guard let encoded = await cfwRenderer.encodeVector(
|
|
3129
|
-
frame, boxes: boxes, cacheGeneration: cfwCacheGeneration, invalidate: cfwSceneInvalid
|
|
3134
|
+
frame, boxes: boxes, cacheGeneration: cfwCacheGeneration, invalidate: cfwSceneInvalid,
|
|
3135
|
+
inlineText: supportsCfwInlineText
|
|
3130
3136
|
) else {
|
|
3131
3137
|
Bridge.log("G2: vector scene does not fit; using the raster path for this frame")
|
|
3132
3138
|
cfwSceneInvalid = true
|
|
@@ -68,7 +68,7 @@ actor G2CfwRenderer {
|
|
|
68
68
|
/// `invalidate` forces a full re-SET (the on-glass slot table is unknown).
|
|
69
69
|
/// Nil when the frame does not fit the slot table or the texture cache.
|
|
70
70
|
func encodeVector(
|
|
71
|
-
_ frame: SceneFrame, boxes: LensBoxes, cacheGeneration: Int, invalidate: Bool
|
|
71
|
+
_ frame: SceneFrame, boxes: LensBoxes, cacheGeneration: Int, invalidate: Bool, inlineText: Bool
|
|
72
72
|
) -> G2CfwScene.Encoded? {
|
|
73
73
|
if textureCache.generation != cacheGeneration || textureCache.exhausted {
|
|
74
74
|
textureCache.reset(generation: cacheGeneration)
|
|
@@ -90,13 +90,15 @@ actor G2CfwRenderer {
|
|
|
90
90
|
}
|
|
91
91
|
let ox = Int(boxes.left.x)
|
|
92
92
|
let oy = Int(boxes.left.y)
|
|
93
|
-
if let encoded = scene.encode(frame, originX: ox, originY: oy,
|
|
93
|
+
if let encoded = scene.encode(frame, originX: ox, originY: oy, inlineText: inlineText,
|
|
94
|
+
cache: textureCache, rasterize: rasterize) {
|
|
94
95
|
return encoded
|
|
95
96
|
}
|
|
96
97
|
// Out of texture cache mid-frame: start a fresh cache and repack once.
|
|
97
98
|
textureCache.reset(generation: cacheGeneration)
|
|
98
99
|
scene.reset(generation: cacheGeneration)
|
|
99
|
-
return scene.encode(frame, originX: ox, originY: oy,
|
|
100
|
+
return scene.encode(frame, originX: ox, originY: oy, inlineText: inlineText,
|
|
101
|
+
cache: textureCache, rasterize: rasterize)
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/// Fixed cell metrics of the phone's monospaced face at `fontSize`.
|
|
@@ -3,19 +3,22 @@ import Foundation
|
|
|
3
3
|
/// Retained vector scene for the Glassly CFW (firmware modes 16-18).
|
|
4
4
|
///
|
|
5
5
|
/// Every scene element becomes one or more firmware slots (paint order = slot
|
|
6
|
-
/// order): vector shapes map 1:1
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
/// tiled to the 255 px slot limit.
|
|
10
|
-
/// layout: unchanged slots cost nothing,
|
|
11
|
-
/// transition become firmware TWEENs, and
|
|
12
|
-
/// stay stable per element id; a reorder
|
|
6
|
+
/// order): vector shapes map 1:1; default and builtin text becomes one inline
|
|
7
|
+
/// TEXT slot per line drawn by the glasses' font (or a cache-backed TEXT slot on
|
|
8
|
+
/// firmware without inline records); mono text and images are rasterized on
|
|
9
|
+
/// the phone and drawn as cached IMAGE slots tiled to the 255 px slot limit.
|
|
10
|
+
/// Frames diff against the last committed layout: unchanged slots cost nothing,
|
|
11
|
+
/// geometry changes on an element with a transition become firmware TWEENs, and
|
|
12
|
+
/// anything else is re-SET. Slot indices stay stable per element id; a reorder
|
|
13
|
+
/// or an over-budget frame repacks.
|
|
13
14
|
final class G2CfwScene {
|
|
14
15
|
static let maxSlots = 128
|
|
15
16
|
static let recordBytes = 20
|
|
16
17
|
static let framePeriodMs = 33.0
|
|
17
18
|
static let textLineHeight = 27
|
|
19
|
+
static let textInset = 2
|
|
18
20
|
static let maxTextBytes = 64
|
|
21
|
+
static let maxInlineBytes = 128
|
|
19
22
|
static let maxTileSize = 255
|
|
20
23
|
/// Mode-13/15 options: identity ramp, transparent source color 0.
|
|
21
24
|
static let textOptions: UInt8 = 0x1F
|
|
@@ -23,7 +26,7 @@ final class G2CfwScene {
|
|
|
23
26
|
|
|
24
27
|
enum ShapeType: UInt8 {
|
|
25
28
|
case line = 1, rect, rectFill, circle, circleFill, tri, triFill, quad, quadFill
|
|
26
|
-
case bezier2, bezier3, arc, pie, image, text, textCached
|
|
29
|
+
case bezier2, bezier3, arc, pie, image, text, textCached, textInline
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
struct Slot: Equatable {
|
|
@@ -31,14 +34,17 @@ final class G2CfwScene {
|
|
|
31
34
|
var color: UInt8
|
|
32
35
|
var width: UInt8
|
|
33
36
|
var params: [Int16]
|
|
37
|
+
/// TEXT_INLINE bytes carried in the record.
|
|
38
|
+
var text: [UInt8]
|
|
34
39
|
|
|
35
|
-
init(_ type: ShapeType, color: UInt8, width: UInt8 = 0, _ params: [Int16]) {
|
|
40
|
+
init(_ type: ShapeType, color: UInt8, width: UInt8 = 0, _ params: [Int16], text: [UInt8] = []) {
|
|
36
41
|
self.type = type.rawValue
|
|
37
42
|
self.color = color
|
|
38
43
|
self.width = width
|
|
39
44
|
var padded = params
|
|
40
45
|
while padded.count < 8 { padded.append(0) }
|
|
41
46
|
self.params = Array(padded.prefix(8))
|
|
47
|
+
self.text = text
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -80,6 +86,7 @@ final class G2CfwScene {
|
|
|
80
86
|
case .tri, .triFill, .bezier2: return 0x3F
|
|
81
87
|
case .quad, .quadFill, .bezier3: return 0xFF
|
|
82
88
|
case .image, .text, .textCached: return 0x03
|
|
89
|
+
case .textInline: return 0x0F
|
|
83
90
|
case .none: return 0
|
|
84
91
|
}
|
|
85
92
|
}
|
|
@@ -124,8 +131,9 @@ final class G2CfwScene {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
/// Slots for one element, or nil when the texture cache cannot hold its content.
|
|
134
|
+
/// `inlineText` selects TEXT_INLINE records for default/builtin text.
|
|
127
135
|
func slots(
|
|
128
|
-
for element: SceneElement, originX ox: Int, originY oy: Int,
|
|
136
|
+
for element: SceneElement, originX ox: Int, originY oy: Int, inlineText: Bool,
|
|
129
137
|
cache: G2CfwTextureCache, rasterize: (SceneElement, Int, Int) -> Data?
|
|
130
138
|
) -> [Slot]? {
|
|
131
139
|
let x = Self.clampCoord(Int(element.x) + ox)
|
|
@@ -179,6 +187,10 @@ final class G2CfwScene {
|
|
|
179
187
|
params.append(contentsOf: [px, py])
|
|
180
188
|
}
|
|
181
189
|
return [Slot(n == 3 ? .bezier2 : .bezier3, color: color, width: Self.stroke(element), params)]
|
|
190
|
+
case "text" where element.font == "mono":
|
|
191
|
+
return rasterSlots(element, x: x, y: y, cache: cache, rasterize: rasterize)
|
|
192
|
+
case "text" where inlineText:
|
|
193
|
+
return inlineTextSlots(element, x: x, y: y)
|
|
182
194
|
case "text" where element.font == "builtin":
|
|
183
195
|
return builtinTextSlots(element, x: x, y: y, cache: cache)
|
|
184
196
|
case "text", "image":
|
|
@@ -208,13 +220,40 @@ final class G2CfwScene {
|
|
|
208
220
|
return out
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
/// One TEXT_INLINE slot per line, drawn by the glasses' font at the same 2 px
|
|
224
|
+
/// inset as phone-rasterized text, clipped to the box.
|
|
225
|
+
private func inlineTextSlots(_ element: SceneElement, x: Int16, y: Int16) -> [Slot] {
|
|
226
|
+
var out: [Slot] = []
|
|
227
|
+
let w = Int16(max(0, min(2047, Int(element.w))))
|
|
228
|
+
let h = Int16(max(0, min(2047, Int(element.h))))
|
|
229
|
+
if element.border > 0 {
|
|
230
|
+
out.append(Slot(.rect, color: Self.gray(element), width: Self.stroke(element),
|
|
231
|
+
[x, y, w, h, Int16(max(0, min(2047, Int(element.radius))))]))
|
|
232
|
+
}
|
|
233
|
+
let options = UInt8(0x10) | Self.gray(element)
|
|
234
|
+
let maxLines = max(1, Int(element.h) / Self.textLineHeight)
|
|
235
|
+
let lines = (element.text ?? "").split(separator: "\n", omittingEmptySubsequences: false).prefix(maxLines)
|
|
236
|
+
for (index, line) in lines.enumerated() {
|
|
237
|
+
let bytes = Self.textBytes(String(line), max: Self.maxInlineBytes)
|
|
238
|
+
if bytes.isEmpty { continue }
|
|
239
|
+
let lineY = Int(y) + Self.textInset + index * Self.textLineHeight
|
|
240
|
+
let clipW = max(0, Int(element.w) - Self.textInset)
|
|
241
|
+
let clipH = max(0, Int(element.h) - Self.textInset - index * Self.textLineHeight)
|
|
242
|
+
out.append(Slot(.textInline, color: options,
|
|
243
|
+
[Self.clampCoord(Int(x) + Self.textInset), Self.clampCoord(lineY),
|
|
244
|
+
Int16(min(2047, clipW)), Int16(min(2047, clipH)), Int16(bytes.count)],
|
|
245
|
+
text: bytes))
|
|
246
|
+
}
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
249
|
+
|
|
211
250
|
/// UTF-8 bytes of a line without firmware control bytes (1..31), cut at a codepoint boundary.
|
|
212
|
-
static func textBytes(_ line: String) -> [UInt8] {
|
|
251
|
+
static func textBytes(_ line: String, max limit: Int = maxTextBytes) -> [UInt8] {
|
|
213
252
|
let cleaned = line.unicodeScalars.filter { $0.value >= 32 }
|
|
214
253
|
var bytes: [UInt8] = []
|
|
215
254
|
for scalar in cleaned {
|
|
216
255
|
let encoded = Array(String(scalar).utf8)
|
|
217
|
-
if bytes.count + encoded.count >
|
|
256
|
+
if bytes.count + encoded.count > limit { break }
|
|
218
257
|
bytes.append(contentsOf: encoded)
|
|
219
258
|
}
|
|
220
259
|
return bytes
|
|
@@ -256,8 +295,8 @@ final class G2CfwScene {
|
|
|
256
295
|
/// that must precede it. Nil when the frame does not fit the slot table or
|
|
257
296
|
/// the texture cache; the caller then falls back to the raster path.
|
|
258
297
|
func encode(
|
|
259
|
-
_ frame: SceneFrame, originX: Int, originY: Int,
|
|
260
|
-
rasterize: (SceneElement, Int, Int) -> Data?
|
|
298
|
+
_ frame: SceneFrame, originX: Int, originY: Int, inlineText: Bool = false,
|
|
299
|
+
cache: G2CfwTextureCache, rasterize: (SceneElement, Int, Int) -> Data?
|
|
261
300
|
) -> Encoded? {
|
|
262
301
|
struct Desired {
|
|
263
302
|
let id: String
|
|
@@ -268,7 +307,8 @@ final class G2CfwScene {
|
|
|
268
307
|
}
|
|
269
308
|
var desired: [Desired] = []
|
|
270
309
|
for element in frame.elements {
|
|
271
|
-
guard let slots = slots(for: element, originX: originX, originY: originY,
|
|
310
|
+
guard let slots = slots(for: element, originX: originX, originY: originY, inlineText: inlineText,
|
|
311
|
+
cache: cache, rasterize: rasterize)
|
|
272
312
|
else { return nil }
|
|
273
313
|
desired.append(Desired(
|
|
274
314
|
id: element.id, contentHash: element.contentHash, slots: slots,
|
|
@@ -347,21 +387,27 @@ final class G2CfwScene {
|
|
|
347
387
|
return Encoded(payload: payload, uploads: cache.takeUploadPayloads())
|
|
348
388
|
}
|
|
349
389
|
|
|
350
|
-
/// `[0][slot][type][flags=visible][color][width][p0..p7 LE]
|
|
390
|
+
/// `[0][slot][type][flags=visible][color][width][p0..p7 LE]`, or for TEXT_INLINE
|
|
391
|
+
/// `[0][slot][17][1][color][width][x][y][w][h][len][bytes]`.
|
|
351
392
|
static func setRecord(_ slot: Slot, index: UInt8) -> Data {
|
|
352
393
|
var out = Data([0, index, slot.type, 1, slot.color, slot.width])
|
|
353
|
-
|
|
394
|
+
let count = slot.type == ShapeType.textInline.rawValue ? 4 : 8
|
|
395
|
+
for value in slot.params.prefix(count) {
|
|
354
396
|
let bits = UInt16(bitPattern: value)
|
|
355
397
|
out.append(UInt8(bits & 0xFF))
|
|
356
398
|
out.append(UInt8(bits >> 8))
|
|
357
399
|
}
|
|
400
|
+
if slot.type == ShapeType.textInline.rawValue {
|
|
401
|
+
out.append(UInt8(min(slot.text.count, maxInlineBytes)))
|
|
402
|
+
out.append(contentsOf: slot.text.prefix(maxInlineBytes))
|
|
403
|
+
}
|
|
358
404
|
return out
|
|
359
405
|
}
|
|
360
406
|
|
|
361
407
|
/// `[5][slot][mask:u16][frames][curve x4][value:i16 per set bit]`, or nil when the
|
|
362
408
|
/// change is not a pure geometry/stroke change of the same shape type.
|
|
363
409
|
static func tweenRecord(from old: Slot, to new: Slot, index: UInt8, frames: UInt8, curve: [UInt8]) -> Data? {
|
|
364
|
-
guard old.type == new.type else { return nil }
|
|
410
|
+
guard old.type == new.type, old.text == new.text else { return nil }
|
|
365
411
|
let tweenable = tweenableMask(new.type)
|
|
366
412
|
var mask: UInt16 = 0
|
|
367
413
|
var values: [Int16] = []
|
|
@@ -372,7 +418,9 @@ final class G2CfwScene {
|
|
|
372
418
|
}
|
|
373
419
|
let isTexture = new.type >= ShapeType.image.rawValue
|
|
374
420
|
if old.color != new.color {
|
|
375
|
-
|
|
421
|
+
// Inline text may fade (low nibble) as long as its option bits stay put.
|
|
422
|
+
let inlineFade = new.type == ShapeType.textInline.rawValue && (old.color & 0xF0) == (new.color & 0xF0)
|
|
423
|
+
guard !isTexture || inlineFade else { return nil }
|
|
376
424
|
mask |= 0x100
|
|
377
425
|
values.append(Int16(new.color))
|
|
378
426
|
}
|
|
@@ -37,8 +37,8 @@ enum G2FirmwareCatalog {
|
|
|
37
37
|
displayName: "Glassly CFW",
|
|
38
38
|
version: "2.2.9.22 CFW",
|
|
39
39
|
fileName: "g2-glassly-cfw-2.2.9.22.evenota.bin",
|
|
40
|
-
sha256: "
|
|
41
|
-
size:
|
|
40
|
+
sha256: "560d4d94344573d11ba58690221c0ea7d5a5e8c8d29c12f158e81078484b815c",
|
|
41
|
+
size: 4_528_184
|
|
42
42
|
),
|
|
43
43
|
]
|
|
44
44
|
|
|
@@ -21,8 +21,9 @@ final class G2CfwSceneTests: XCTestCase {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
private func encode(_ scene: G2CfwScene, _ cache: G2CfwTextureCache, _ elements: [SceneElement],
|
|
24
|
-
replay: Bool = false) -> G2CfwScene.Encoded? {
|
|
25
|
-
scene.encode(frame(elements, replay: replay), originX: 32, originY: 96,
|
|
24
|
+
replay: Bool = false, inlineText: Bool = false) -> G2CfwScene.Encoded? {
|
|
25
|
+
scene.encode(frame(elements, replay: replay), originX: 32, originY: 96, inlineText: inlineText,
|
|
26
|
+
cache: cache) { el, w, h in
|
|
26
27
|
Data(repeating: 0xFF, count: w * h)
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -184,4 +185,47 @@ final class G2CfwSceneTests: XCTestCase {
|
|
|
184
185
|
XCTAssertEqual(G2CfwScene.transitionFrames(ms: 60000), 255)
|
|
185
186
|
XCTAssertEqual(G2CfwScene.easingCurve("ease-in-out"), [107, 0, 148, 255])
|
|
186
187
|
}
|
|
188
|
+
|
|
189
|
+
func testInlineTextRecordsCarryTheirBytesAndAnimate() throws {
|
|
190
|
+
let scene = G2CfwScene()
|
|
191
|
+
let cache = G2CfwTextureCache()
|
|
192
|
+
cache.reset(generation: 1)
|
|
193
|
+
scene.reset(generation: 1)
|
|
194
|
+
let first = try XCTUnwrap(encode(scene, cache, [
|
|
195
|
+
element("t", type: "text", x: 10, y: 10, w: 200, h: 60, text: "hi\nthere", color: 9),
|
|
196
|
+
element("m", type: "text", x: 0, y: 200, w: 100, h: 30, text: "grid", font: "mono", hash: "mono"),
|
|
197
|
+
], inlineText: true))
|
|
198
|
+
XCTAssertEqual(first.uploads.count, 1) // only the mono raster hits the cache
|
|
199
|
+
XCTAssertEqual(scene.elements.map(\.indices), [[0, 1], [2]])
|
|
200
|
+
let bytes = [UInt8](first.payload)
|
|
201
|
+
// slot 0: TEXT_INLINE, transparent + gray 9, x/y inset by 2, clip to the box, "hi"
|
|
202
|
+
XCTAssertEqual(Array(bytes[3 ..< 9]), [0, 0, 17, 1, 0x19, 0])
|
|
203
|
+
XCTAssertEqual(Array(bytes[9 ..< 17]), le(44, 108, 198, 58))
|
|
204
|
+
XCTAssertEqual(Array(bytes[17 ..< 20]), [2, 0x68, 0x69])
|
|
205
|
+
// slot 1 follows immediately: 13 + 2 bytes after the first record
|
|
206
|
+
XCTAssertEqual(Array(bytes[20 ..< 23]), [0, 1, 17])
|
|
207
|
+
XCTAssertEqual(Array(bytes[26 ..< 34]), le(44, 135, 198, 31))
|
|
208
|
+
XCTAssertEqual(Array(bytes[35 ..< 40]), Array("there".utf8))
|
|
209
|
+
XCTAssertEqual(bytes[40], 0) // mono raster SET
|
|
210
|
+
XCTAssertEqual(bytes[42], 14)
|
|
211
|
+
|
|
212
|
+
// a new string is a SET even with a transition; a move alone tweens; a fade tweens the color
|
|
213
|
+
let moved = try XCTUnwrap(encode(scene, cache, [
|
|
214
|
+
element("t", type: "text", x: 10, y: 40, w: 200, h: 60, text: "hi\nthere", color: 9, transitionMs: 330),
|
|
215
|
+
element("m", type: "text", x: 0, y: 200, w: 100, h: 30, text: "grid", font: "mono", hash: "mono"),
|
|
216
|
+
], inlineText: true))
|
|
217
|
+
XCTAssertEqual(Array([UInt8](moved.payload)[3 ..< 8]), [5, 0, 0x02, 0x00, 10])
|
|
218
|
+
let retexted = try XCTUnwrap(encode(scene, cache, [
|
|
219
|
+
element("t", type: "text", x: 10, y: 40, w: 200, h: 60, text: "ho\nthere", color: 9, transitionMs: 330),
|
|
220
|
+
element("m", type: "text", x: 0, y: 200, w: 100, h: 30, text: "grid", font: "mono", hash: "mono"),
|
|
221
|
+
], inlineText: true))
|
|
222
|
+
XCTAssertEqual(Array([UInt8](retexted.payload)[3 ..< 6]), [0, 0, 17])
|
|
223
|
+
let faded = try XCTUnwrap(encode(scene, cache, [
|
|
224
|
+
element("t", type: "text", x: 10, y: 40, w: 200, h: 60, text: "ho\nthere", color: 3, transitionMs: 330),
|
|
225
|
+
element("m", type: "text", x: 0, y: 200, w: 100, h: 30, text: "grid", font: "mono", hash: "mono"),
|
|
226
|
+
], inlineText: true))
|
|
227
|
+
let fade = [UInt8](faded.payload)
|
|
228
|
+
XCTAssertEqual(Array(fade[3 ..< 8]), [5, 0, 0x00, 0x01, 10])
|
|
229
|
+
XCTAssertEqual(Array(fade[12 ..< 14]), le(0x13))
|
|
230
|
+
}
|
|
187
231
|
}
|
package/package.json
CHANGED
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.1.0",
|
|
15
|
-
"releaseIdentity": "0.1.0-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.1.0-dev.113",
|
|
16
|
+
"releaseSetId": "glassly-0.1.0-dev.113",
|
|
17
|
+
"sourceCommit": "e63da43b467758373b960b0ee9167ef29938a11c",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|