@glassly/bluetooth-sdk 0.1.0-dev.98 → 0.2.8-dev.444
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/README.md +47 -6
- package/android/build.gradle +7 -7
- package/android/lc3Lib/build.gradle +2 -2
- package/android/src/main/assets/G2Firmware/README.md +27 -2
- package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1075 -257
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1434 -504
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +24 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2CfwSceneTests: XCTestCase {
|
|
6
|
+
private func element(
|
|
7
|
+
_ id: String, type: String, x: Int32 = 0, y: Int32 = 0, w: Int32 = 0, h: Int32 = 0,
|
|
8
|
+
text: String? = nil, border: Int32 = 0, radius: Int32 = 0, font: String = "default", fontSize: Int32 = 0,
|
|
9
|
+
fill: Bool = false, color: Int32 = -1, points: [Int32] = [], startDeg: Int32 = 0, endDeg: Int32 = 0,
|
|
10
|
+
transitionMs: Int32 = 0, easing: String = "ease", hash: String = "h",
|
|
11
|
+
path: [UInt8] = [], pathScale: Int32 = 256, fillRule: Int32 = 0, rotationQ8: Int32 = 0, pivotX: Int32? = nil, pivotY: Int32? = nil,
|
|
12
|
+
depth: Int32 = 0
|
|
13
|
+
) -> SceneElement {
|
|
14
|
+
SceneElement(
|
|
15
|
+
id: id, type: type, x: x, y: y, w: w, h: h, text: text, data: nil, border: border, radius: radius,
|
|
16
|
+
change: "created", contentHash: hash, font: font, fontSize: fontSize, fill: fill, color: color,
|
|
17
|
+
points: points,
|
|
18
|
+
startDeg: startDeg, endDeg: endDeg, transitionMs: transitionMs, easing: easing,
|
|
19
|
+
path: path, pathScale: pathScale, fillRule: fillRule, rotationQ8: rotationQ8, pivotX: pivotX, pivotY: pivotY,
|
|
20
|
+
depth: depth
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func frame(_ elements: [SceneElement], replay: Bool = false) -> SceneFrame {
|
|
25
|
+
SceneFrame(appId: "app", epoch: 1, replay: replay, elements: elements, removed: [])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private func encode(_ scene: G2CfwScene, _ cache: G2CfwTextureCache, _ elements: [SceneElement],
|
|
29
|
+
replay: Bool = false, inlineText: Bool = false, tag: Int? = nil, parallax: Int = 0) -> G2CfwScene.Encoded?
|
|
30
|
+
{
|
|
31
|
+
scene.encode(frame(elements, replay: replay), originX: 32, originY: 96, inlineText: inlineText,
|
|
32
|
+
tag: tag, parallax: parallax, cache: cache)
|
|
33
|
+
{ _, w, h in
|
|
34
|
+
Data(repeating: 0xFF, count: w * h)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Little-endian int16 bytes of each value, concatenated.
|
|
39
|
+
private func le(_ values: Int...) -> [UInt8] {
|
|
40
|
+
var out: [UInt8] = []
|
|
41
|
+
for v in values {
|
|
42
|
+
let bits = UInt16(bitPattern: Int16(v))
|
|
43
|
+
out.append(UInt8(bits & 0xFF))
|
|
44
|
+
out.append(UInt8(bits >> 8))
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func u32(_ v: UInt32) -> [UInt8] {
|
|
50
|
+
[UInt8(v & 0xFF), UInt8((v >> 8) & 0xFF), UInt8((v >> 16) & 0xFF), UInt8((v >> 24) & 0xFF)]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// `[38][epoch 0][request][flags][bg][puts]`
|
|
54
|
+
private let header = 8
|
|
55
|
+
|
|
56
|
+
/// Split a SHOW into its parts by walking the wire format.
|
|
57
|
+
private func parse(_ payload: Data) -> (request: Int, flags: UInt8, puts: [[UInt8]], refs: [(UInt32, UInt32)], ops: [UInt8]) {
|
|
58
|
+
let b = [UInt8](payload)
|
|
59
|
+
XCTAssertEqual(b[0], 38)
|
|
60
|
+
XCTAssertEqual(Array(b[1 ..< 3]), [0, 0], "the encoder leaves the epoch to the link")
|
|
61
|
+
let request = Int(b[3]) | Int(b[4]) << 8
|
|
62
|
+
var pos = header
|
|
63
|
+
var puts: [[UInt8]] = []
|
|
64
|
+
for _ in 0 ..< Int(b[7]) {
|
|
65
|
+
XCTAssertEqual(b[pos], 1)
|
|
66
|
+
let type = b[pos + 9]
|
|
67
|
+
let recordLength: Int
|
|
68
|
+
switch type {
|
|
69
|
+
case 17: recordLength = 13 + Int(b[pos + 9 + 12])
|
|
70
|
+
case 18: recordLength = 12 + Int(b[pos + 9 + 10]) | Int(b[pos + 9 + 11]) << 8
|
|
71
|
+
case 14, 15: recordLength = 12
|
|
72
|
+
case 16: recordLength = 16
|
|
73
|
+
default: recordLength = 20
|
|
74
|
+
}
|
|
75
|
+
puts.append(Array(b[pos ..< pos + 9 + recordLength]))
|
|
76
|
+
pos += 9 + recordLength
|
|
77
|
+
}
|
|
78
|
+
let count = Int(b[pos]) | Int(b[pos + 1]) << 8
|
|
79
|
+
pos += 2
|
|
80
|
+
var refs: [(UInt32, UInt32)] = []
|
|
81
|
+
for _ in 0 ..< count {
|
|
82
|
+
let id = UInt32(b[pos]) | UInt32(b[pos + 1]) << 8 | UInt32(b[pos + 2]) << 16 | UInt32(b[pos + 3]) << 24
|
|
83
|
+
let ver = UInt32(b[pos + 4]) | UInt32(b[pos + 5]) << 8 | UInt32(b[pos + 6]) << 16 | UInt32(b[pos + 7]) << 24
|
|
84
|
+
refs.append((id, ver))
|
|
85
|
+
pos += 8
|
|
86
|
+
}
|
|
87
|
+
return (request, b[5], puts, refs, Array(b[pos...]))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func fresh() -> (G2CfwScene, G2CfwTextureCache) {
|
|
91
|
+
let scene = G2CfwScene(), cache = G2CfwTextureCache()
|
|
92
|
+
scene.reset(generation: 1); cache.reset(generation: 1)
|
|
93
|
+
return (scene, cache)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The warm-up is one SHOW: a black 1 px dot defined, referenced and tweened for two frames.
|
|
97
|
+
func testWarmUpIsOneShowOfADotWithATween() {
|
|
98
|
+
let p = parse(G2CfwScene.warmUpShow)
|
|
99
|
+
XCTAssertEqual(p.flags, 0x01)
|
|
100
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
101
|
+
XCTAssertEqual(Array(p.puts[0][9 ..< 13]), [5, 1, 0, 0])
|
|
102
|
+
XCTAssertEqual(p.refs.count, 1)
|
|
103
|
+
XCTAssertEqual(p.refs[0].0, G2CfwScene.warmUpId)
|
|
104
|
+
XCTAssertEqual(Array(p.ops[0 ..< 5]), [5] + u32(G2CfwScene.warmUpId))
|
|
105
|
+
XCTAssertEqual(Array(p.ops[5 ..< 12]), [0x01, 0x00, 2, 0, 0, 255, 255])
|
|
106
|
+
XCTAssertEqual(G2CfwScene.hidePayload, Data([39, 0, 0, 0, 0, 1, 0]))
|
|
107
|
+
XCTAssertEqual(G2CfwScene.resetPayload, Data([41, 0, 0, 0, 0, 0]))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func testShapesBecomeObjectDefinitionsWithOffsetGeometry() throws {
|
|
111
|
+
let (scene, cache) = fresh()
|
|
112
|
+
let encoded = try XCTUnwrap(encode(scene, cache, [
|
|
113
|
+
element("l", type: "line", border: 3, color: 12, points: [10, 20, 30, 40]),
|
|
114
|
+
element("c", type: "circle", x: 100, y: 100, w: 41, h: 41, fill: true),
|
|
115
|
+
element("a", type: "arc", x: 0, y: 0, w: 101, h: 101, border: 4, startDeg: -90, endDeg: 180),
|
|
116
|
+
element("r", type: "rect", x: 1, y: 2, w: 30, h: 20, border: 2, radius: 6, fill: true),
|
|
117
|
+
], tag: 77))
|
|
118
|
+
let p = parse(encoded.payload)
|
|
119
|
+
XCTAssertEqual(p.request, 77)
|
|
120
|
+
XCTAssertEqual(p.flags, 0x05, "PRESENT + TAG")
|
|
121
|
+
XCTAssertEqual(p.puts.count, 5)
|
|
122
|
+
XCTAssertEqual(Array(p.puts[0][1 ..< 5]), u32(G2CfwScene.objectId("l", 0)))
|
|
123
|
+
XCTAssertEqual(Array(p.puts[0][9 ..< 13]), [1, 1, 12, 3])
|
|
124
|
+
XCTAssertEqual(Array(p.puts[0][13 ..< 21]), le(42, 116, 62, 136))
|
|
125
|
+
XCTAssertEqual(Array(p.puts[1][9 ..< 13]), [5, 1, 15, 0])
|
|
126
|
+
XCTAssertEqual(Array(p.puts[1][13 ..< 19]), le(152, 216, 20))
|
|
127
|
+
XCTAssertEqual(Array(p.puts[2][9 ..< 13]), [12, 1, 15, 4])
|
|
128
|
+
XCTAssertEqual(Array(p.puts[2][19 ..< 23]), le(-90, 180))
|
|
129
|
+
XCTAssertEqual(Array(p.puts[3][9 ..< 13]), [3, 1, 15, 0])
|
|
130
|
+
XCTAssertEqual(Array(p.puts[4][9 ..< 13]), [2, 1, 15, 2])
|
|
131
|
+
XCTAssertEqual(p.refs.map { $0.0 }, [G2CfwScene.objectId("l", 0), G2CfwScene.objectId("c", 0), G2CfwScene.objectId("a", 0), G2CfwScene.objectId("r", 0), G2CfwScene.objectId("r", 1)])
|
|
132
|
+
let putVersion = UInt32(p.puts[0][5]) | UInt32(p.puts[0][6]) << 8
|
|
133
|
+
let putVersionHigh = UInt32(p.puts[0][7]) << 16 | UInt32(p.puts[0][8]) << 24
|
|
134
|
+
XCTAssertEqual(p.refs[0].1, putVersion | putVersionHigh)
|
|
135
|
+
XCTAssertTrue(p.ops.isEmpty)
|
|
136
|
+
XCTAssertTrue(encoded.uploads.isEmpty)
|
|
137
|
+
XCTAssertTrue(encoded.changed)
|
|
138
|
+
XCTAssertEqual(encoded.puts, 5)
|
|
139
|
+
XCTAssertEqual(scene.objectIds, [[G2CfwScene.objectId("l", 0)], [G2CfwScene.objectId("c", 0)], [G2CfwScene.objectId("a", 0)], [G2CfwScene.objectId("r", 0), G2CfwScene.objectId("r", 1)]])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func testSvgDefinitionAndTransformOpsMatchTheWireFormat() throws {
|
|
143
|
+
let (scene, cache) = fresh()
|
|
144
|
+
let path: [UInt8] = [0, 0, 0, 0, 0, 1, 160, 0, 0, 0, 1, 160, 0, 160, 0, 4]
|
|
145
|
+
let svg = element("s", type: "svg", x: 10, y: 20, path: path, pathScale: 512, fillRule: 1, rotationQ8: 90 * 256, pivotX: 40, pivotY: 50)
|
|
146
|
+
let first = try XCTUnwrap(encode(scene, cache, [svg]))
|
|
147
|
+
let p = parse(first.payload)
|
|
148
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
149
|
+
let record = Array(p.puts[0][9...])
|
|
150
|
+
XCTAssertEqual(Array(record[0 ..< 4]), [18, 1, 15, 1])
|
|
151
|
+
XCTAssertEqual(Array(record[4 ..< 12]), le(42, 116, 512) + [16, 0])
|
|
152
|
+
XCTAssertEqual(Array(record[12...]), path)
|
|
153
|
+
// rotated at definition time: a 0 ms ROTATE follows
|
|
154
|
+
let id = G2CfwScene.objectId("s", 0)
|
|
155
|
+
XCTAssertEqual(Array(p.ops[0 ..< 5]), [9] + u32(id))
|
|
156
|
+
XCTAssertEqual(Array(p.ops[5 ..< 9]), [0, 90, 0, 0])
|
|
157
|
+
XCTAssertEqual(Array(p.ops[9 ..< 19]), le(72, 146, 0) + [64, 26, 64, 255])
|
|
158
|
+
// move + scale + rotate with a transition: a TWEEN and a ROTATE on a pure reference
|
|
159
|
+
let moved = element("s", type: "svg", x: 30, y: 20, transitionMs: 330, easing: "linear", path: path, pathScale: 256, fillRule: 1, rotationQ8: 180 * 256, pivotX: 40, pivotY: 50)
|
|
160
|
+
let second = try XCTUnwrap(encode(scene, cache, [moved]))
|
|
161
|
+
let q = parse(second.payload)
|
|
162
|
+
XCTAssertTrue(q.puts.isEmpty)
|
|
163
|
+
XCTAssertEqual(q.refs.count, 1)
|
|
164
|
+
XCTAssertEqual(Array(q.ops[0 ..< 12]), [5] + u32(id) + [0x07, 0x01, 10, 0, 0, 255, 255])
|
|
165
|
+
XCTAssertEqual(Array(q.ops[12 ..< 20]), le(62, 116, 256, 15))
|
|
166
|
+
XCTAssertEqual(Array(q.ops[20 ..< 25]), [9] + u32(id))
|
|
167
|
+
XCTAssertEqual(Array(q.ops[25 ..< 29]), [0, 180, 0, 0])
|
|
168
|
+
XCTAssertEqual(Array(q.ops[33 ..< 35]), le(330))
|
|
169
|
+
XCTAssertEqual(second.hits, 0)
|
|
170
|
+
XCTAssertEqual(second.tweens, 1)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func testMovedElementWithTransitionBecomesTween() throws {
|
|
174
|
+
let (scene, cache) = fresh()
|
|
175
|
+
_ = encode(scene, cache, [element("ball", type: "circle", x: 0, y: 0, w: 21, h: 21, fill: true)])
|
|
176
|
+
let encoded = try XCTUnwrap(encode(scene, cache, [
|
|
177
|
+
element("ball", type: "circle", x: 200, y: 0, w: 21, h: 21, fill: true, transitionMs: 330, easing: "linear"),
|
|
178
|
+
]))
|
|
179
|
+
let p = parse(encoded.payload)
|
|
180
|
+
let id = G2CfwScene.objectId("ball", 0)
|
|
181
|
+
XCTAssertTrue(p.puts.isEmpty)
|
|
182
|
+
XCTAssertEqual(p.refs.map { $0.0 }, [id])
|
|
183
|
+
XCTAssertEqual(Array(p.ops[0 ..< 12]), [5] + u32(id) + [0x07, 0x03, 10, 0, 0, 255, 255])
|
|
184
|
+
XCTAssertEqual(Array(p.ops[12 ..< 22]), le(242, 106, 10, 15, 0)) // cx cy r color width: unchanged ones ride along
|
|
185
|
+
XCTAssertEqual(p.ops.count, 22)
|
|
186
|
+
XCTAssertEqual(encoded.hits, 0)
|
|
187
|
+
XCTAssertEqual(encoded.tweens, 1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func testGeometryChangeWithoutTransitionAndTypeChangeRedefine() throws {
|
|
191
|
+
let (scene, cache) = fresh()
|
|
192
|
+
_ = encode(scene, cache, [element("s", type: "circle", x: 0, y: 0, w: 11, h: 11)])
|
|
193
|
+
let moved = try XCTUnwrap(encode(scene, cache, [element("s", type: "circle", x: 5, y: 0, w: 11, h: 11)]))
|
|
194
|
+
var p = parse(moved.payload)
|
|
195
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
196
|
+
XCTAssertTrue(p.ops.isEmpty)
|
|
197
|
+
let retyped = try XCTUnwrap(encode(scene, cache, [element("s", type: "circle", x: 5, y: 0, w: 11, h: 11, fill: true, transitionMs: 500)]))
|
|
198
|
+
p = parse(retyped.payload)
|
|
199
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
200
|
+
XCTAssertEqual(p.puts[0][9], 5)
|
|
201
|
+
XCTAssertTrue(p.ops.isEmpty)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func testRemovalLeavesTheListAndReappearanceIsAReference() throws {
|
|
205
|
+
let (scene, cache) = fresh()
|
|
206
|
+
let a = element("a", type: "circle", x: 0, y: 0, w: 11, h: 11), b = element("b", type: "circle", x: 0, y: 0, w: 11, h: 11)
|
|
207
|
+
_ = encode(scene, cache, [a, b])
|
|
208
|
+
let onlyB = try XCTUnwrap(encode(scene, cache, [b]))
|
|
209
|
+
var p = parse(onlyB.payload)
|
|
210
|
+
XCTAssertTrue(p.puts.isEmpty)
|
|
211
|
+
XCTAssertEqual(p.refs.map { $0.0 }, [G2CfwScene.objectId("b", 0)])
|
|
212
|
+
XCTAssertTrue(onlyB.changed)
|
|
213
|
+
XCTAssertEqual(scene.known.count, 2, "a stays cached")
|
|
214
|
+
let both = try XCTUnwrap(encode(scene, cache, [a, b]))
|
|
215
|
+
p = parse(both.payload)
|
|
216
|
+
XCTAssertTrue(p.puts.isEmpty)
|
|
217
|
+
XCTAssertEqual(p.refs.count, 2)
|
|
218
|
+
XCTAssertEqual(both.hits, 2)
|
|
219
|
+
// the same frame again is nothing to send
|
|
220
|
+
let same = try XCTUnwrap(encode(scene, cache, [a, b]))
|
|
221
|
+
XCTAssertFalse(same.changed)
|
|
222
|
+
// after a hide the same frame is a SHOW of references again
|
|
223
|
+
scene.hidden()
|
|
224
|
+
let reopen = try XCTUnwrap(encode(scene, cache, [a, b]))
|
|
225
|
+
XCTAssertTrue(reopen.changed)
|
|
226
|
+
XCTAssertEqual(reopen.hits, 2)
|
|
227
|
+
XCTAssertEqual(parse(reopen.payload).puts.count, 0)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func testHiddenMidTweenObjectsAreSnappedWhenShownAgain() throws {
|
|
231
|
+
let (scene, cache) = fresh()
|
|
232
|
+
_ = encode(scene, cache, [element("ball", type: "circle", x: 0, y: 0, w: 21, h: 21, fill: true)])
|
|
233
|
+
_ = encode(scene, cache, [element("ball", type: "circle", x: 200, y: 0, w: 21, h: 21, fill: true, transitionMs: 3000)])
|
|
234
|
+
scene.hidden()
|
|
235
|
+
let reopen = try XCTUnwrap(encode(scene, cache, [element("ball", type: "circle", x: 200, y: 0, w: 21, h: 21, fill: true)]))
|
|
236
|
+
let p = parse(reopen.payload)
|
|
237
|
+
XCTAssertTrue(p.puts.isEmpty)
|
|
238
|
+
XCTAssertEqual(Array(p.ops[0 ..< 12]), [5] + u32(G2CfwScene.objectId("ball", 0)) + [0x07, 0x03, 0, 64, 26, 64, 255])
|
|
239
|
+
XCTAssertEqual(Array(p.ops[12 ..< 16]), le(242, 106))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func testObjectReturningToTheFrameSnapsInsteadOfTweeningFromWhereItLeft() throws {
|
|
243
|
+
let (scene, cache) = fresh()
|
|
244
|
+
let bg = element("bg", type: "rect", w: 50, h: 10, fill: true)
|
|
245
|
+
func ball(_ x: Int32, _ ms: Int32 = 0) -> SceneElement {
|
|
246
|
+
element("ball", type: "circle", x: x, w: 21, h: 21, fill: true, transitionMs: ms, easing: "linear")
|
|
247
|
+
}
|
|
248
|
+
let id = G2CfwScene.objectId("ball", 0)
|
|
249
|
+
_ = encode(scene, cache, [bg, ball(200)])
|
|
250
|
+
_ = encode(scene, cache, [bg, ball(0, 330)])
|
|
251
|
+
_ = encode(scene, cache, [bg])
|
|
252
|
+
// Back at its resident definition with a transition: snapped there, not slid over from x=0.
|
|
253
|
+
let back = try XCTUnwrap(encode(scene, cache, [bg, ball(200, 330)]))
|
|
254
|
+
let p = parse(back.payload)
|
|
255
|
+
XCTAssertTrue(p.puts.isEmpty)
|
|
256
|
+
XCTAssertEqual(back.tweens, 0)
|
|
257
|
+
XCTAssertEqual(Array(p.ops[0 ..< 12]), [5] + u32(id) + [0x07, 0x03, 0, 0, 0, 255, 255])
|
|
258
|
+
XCTAssertEqual(Array(p.ops[12 ..< 16]), le(242, 106))
|
|
259
|
+
// Once on screen it animates again.
|
|
260
|
+
let slid = try XCTUnwrap(encode(scene, cache, [bg, ball(0, 330)]))
|
|
261
|
+
XCTAssertEqual(slid.tweens, 1)
|
|
262
|
+
XCTAssertEqual(Array(parse(slid.payload).ops[5 ..< 8]), [0x07, 0x03, 10])
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func testForgetRedefinesOnlyTheMissingObjects() throws {
|
|
266
|
+
let (scene, cache) = fresh()
|
|
267
|
+
let a = element("a", type: "circle", x: 0, y: 0, w: 11, h: 11), b = element("b", type: "circle", x: 0, y: 0, w: 11, h: 11)
|
|
268
|
+
_ = encode(scene, cache, [a, b])
|
|
269
|
+
scene.forget(objectIds: [G2CfwScene.objectId("a", 0)])
|
|
270
|
+
let again = try XCTUnwrap(encode(scene, cache, [a, b]))
|
|
271
|
+
let p = parse(again.payload)
|
|
272
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
273
|
+
XCTAssertEqual(Array(p.puts[0][1 ..< 5]), u32(G2CfwScene.objectId("a", 0)))
|
|
274
|
+
XCTAssertEqual(p.refs.count, 2)
|
|
275
|
+
scene.invalidate()
|
|
276
|
+
XCTAssertEqual(parse(try XCTUnwrap(encode(scene, cache, [a, b])).payload).puts.count, 2)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func testBuiltinTextAndRasterTilesUseAssets() throws {
|
|
280
|
+
let (scene, cache) = fresh()
|
|
281
|
+
let encoded = try XCTUnwrap(encode(scene, cache, [
|
|
282
|
+
element("t", type: "text", x: 0, y: 0, w: 100, h: 30, text: "hi", font: "builtin"),
|
|
283
|
+
element("i", type: "image", x: 0, y: 40, w: 300, h: 10, hash: "img"),
|
|
284
|
+
]))
|
|
285
|
+
let p = parse(encoded.payload)
|
|
286
|
+
XCTAssertEqual(p.puts.count, 3)
|
|
287
|
+
XCTAssertEqual(Array(p.puts[0][9 ..< 13]), [15, 1, 0x1F, 0])
|
|
288
|
+
let stringId = G2CfwTextureCache.hash31("t:hi")
|
|
289
|
+
XCTAssertEqual(Array(p.puts[0][17 ..< 21]), u32(stringId))
|
|
290
|
+
XCTAssertEqual(Array(p.puts[1][9 ..< 13]), [14, 1, 0x0F, 0])
|
|
291
|
+
XCTAssertEqual(Array(p.puts[1][13 ..< 17]), le(32, 136))
|
|
292
|
+
XCTAssertEqual(Array(p.puts[2][13 ..< 17]), le(287, 136), "second tile starts at 255")
|
|
293
|
+
// the uploads: one PUT with the string and the two tiles (identical pixels share one asset)
|
|
294
|
+
XCTAssertEqual(encoded.uploads.count, 1)
|
|
295
|
+
let upload = [UInt8](encoded.uploads[0])
|
|
296
|
+
XCTAssertEqual(Array(upload[0 ..< 5]), [37, 0, 0, 0, 0])
|
|
297
|
+
XCTAssertEqual(upload[5], 0)
|
|
298
|
+
XCTAssertEqual(Array(upload[6 ..< 10]), u32(stringId))
|
|
299
|
+
XCTAssertEqual(upload[14], G2CfwTextureCache.kindString)
|
|
300
|
+
XCTAssertEqual(Array(upload[15 ..< 19]), u32(2))
|
|
301
|
+
XCTAssertEqual(Array(upload[25 ..< 27]), Array("hi".utf8))
|
|
302
|
+
XCTAssertEqual(upload[27], 0)
|
|
303
|
+
XCTAssertEqual(upload[36], G2CfwTextureCache.kindImage)
|
|
304
|
+
let tiles = Set([Array(p.puts[1][17 ..< 21]), Array(p.puts[2][17 ..< 21])])
|
|
305
|
+
XCTAssertEqual(tiles.count, 2, "255x10 and 45x10 tiles are distinct assets")
|
|
306
|
+
XCTAssertTrue(encode(scene, cache, [element("t", type: "text", x: 0, y: 0, w: 100, h: 30, text: "hi", font: "builtin")])!.uploads.isEmpty, "resident assets do not re-upload")
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func testImagesShareAssetsByPixelsAndFadeAsTweens() throws {
|
|
310
|
+
let (scene, cache) = fresh()
|
|
311
|
+
let first = try XCTUnwrap(encode(scene, cache, [
|
|
312
|
+
element("a", type: "image", x: 0, y: 0, w: 10, h: 10, color: 15, hash: "one"),
|
|
313
|
+
element("b", type: "image", x: 20, y: 0, w: 10, h: 10, color: 15, hash: "two"),
|
|
314
|
+
]))
|
|
315
|
+
let p = parse(first.payload)
|
|
316
|
+
XCTAssertEqual(Array(p.puts[0][17 ..< 21]), Array(p.puts[1][17 ..< 21]))
|
|
317
|
+
XCTAssertEqual(first.uploads.count, 1)
|
|
318
|
+
let faded = try XCTUnwrap(encode(scene, cache, [
|
|
319
|
+
element("a", type: "image", x: 0, y: 0, w: 10, h: 10, color: 8, transitionMs: 330, hash: "one"),
|
|
320
|
+
element("b", type: "image", x: 20, y: 0, w: 10, h: 10, color: 15, hash: "two"),
|
|
321
|
+
]))
|
|
322
|
+
let q = parse(faded.payload)
|
|
323
|
+
XCTAssertTrue(q.puts.isEmpty)
|
|
324
|
+
XCTAssertEqual(Array(q.ops[5 ..< 8]), [0x03, 0x01, 10])
|
|
325
|
+
XCTAssertEqual(Array(q.ops[12 ..< 18]), le(32, 96, 8))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func testTooManyObjectsReturnsNil() {
|
|
329
|
+
let (scene, cache) = fresh()
|
|
330
|
+
let many = (0 ..< 257).map { element("e\($0)", type: "circle", x: 0, y: 0, w: 5, h: 5) }
|
|
331
|
+
XCTAssertNil(encode(scene, cache, many))
|
|
332
|
+
XCTAssertNotNil(encode(scene, cache, Array(many.prefix(256))))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func testTextBytesStripControlsAndCutAtCodepoints() {
|
|
336
|
+
XCTAssertEqual(G2CfwScene.textBytes("a\u{1}b\u{e9}", max: 3), [0x61, 0x62])
|
|
337
|
+
XCTAssertEqual(G2CfwScene.textBytes("héllo", max: 64), Array("héllo".utf8))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
func testInlineTextRecordsCarryTheirBytesAndAnimate() throws {
|
|
341
|
+
let (scene, cache) = fresh()
|
|
342
|
+
let t = { (y: Int32, text: String, color: Int32, ms: Int32) in
|
|
343
|
+
self.element("t", type: "text", x: 10, y: y, w: 200, h: 60, text: text, color: color, transitionMs: ms)
|
|
344
|
+
}
|
|
345
|
+
let first = try XCTUnwrap(encode(scene, cache, [t(10, "hi\nthere", 9, 0)], inlineText: true))
|
|
346
|
+
let p = parse(first.payload)
|
|
347
|
+
XCTAssertEqual(p.puts.count, 2)
|
|
348
|
+
XCTAssertEqual(Array(p.puts[0][9 ..< 13]), [17, 1, 0x19, 0])
|
|
349
|
+
XCTAssertEqual(Array(p.puts[0][13 ..< 21]), le(44, 108, 198, 58))
|
|
350
|
+
XCTAssertEqual(Array(p.puts[0][21 ..< 24]), [2, 0x68, 0x69])
|
|
351
|
+
XCTAssertEqual(Array(p.puts[1][13 ..< 21]), le(44, 135, 198, 31))
|
|
352
|
+
XCTAssertEqual(Array(p.puts[1][22 ..< 27]), Array("there".utf8))
|
|
353
|
+
XCTAssertTrue(first.uploads.isEmpty)
|
|
354
|
+
let moved = try XCTUnwrap(encode(scene, cache, [t(40, "hi\nthere", 9, 330)], inlineText: true))
|
|
355
|
+
let q = parse(moved.payload)
|
|
356
|
+
XCTAssertTrue(q.puts.isEmpty)
|
|
357
|
+
XCTAssertEqual(Array(q.ops[5 ..< 8]), [0x0F, 0x01, 10])
|
|
358
|
+
XCTAssertEqual(Array(q.ops[12 ..< 22]), le(44, 138, 198, 58, 0x19))
|
|
359
|
+
let changed = try XCTUnwrap(encode(scene, cache, [t(40, "ho\nthere", 9, 330)], inlineText: true))
|
|
360
|
+
XCTAssertEqual(parse(changed.payload).puts.count, 1)
|
|
361
|
+
let faded = try XCTUnwrap(encode(scene, cache, [t(40, "ho\nthere", 3, 330)], inlineText: true))
|
|
362
|
+
let r = parse(faded.payload)
|
|
363
|
+
XCTAssertEqual(r.ops.count, 44)
|
|
364
|
+
XCTAssertEqual(Array(r.ops[20 ..< 22]), le(0x13))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private var mono: G2CfwScene.MonoFace {
|
|
368
|
+
G2CfwScene.MonoFace(metrics: { _ in (6, 12) }) { _, _, w, h in Data(repeating: 0xFF, count: w * h) }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func testMonoTextBecomesCachedFontRunsThatTween() throws {
|
|
372
|
+
let (scene, cache) = fresh()
|
|
373
|
+
let grid = element("g", type: "text", x: 0, y: 0, w: 60, h: 24, text: "ab\ncd", font: "mono", fontSize: 12)
|
|
374
|
+
let encoded = try XCTUnwrap(scene.encode(frame([grid]), originX: 32, originY: 96, inlineText: true, mono: mono, cache: cache) { _, _, _ in nil })
|
|
375
|
+
let p = parse(encoded.payload)
|
|
376
|
+
XCTAssertEqual(p.puts.count, 2)
|
|
377
|
+
XCTAssertEqual(Array(p.puts[0][9 ..< 13]), [16, 1, 15, 0])
|
|
378
|
+
XCTAssertEqual(Array(p.puts[0][13 ..< 17]), le(34, 98))
|
|
379
|
+
let fontId = G2CfwTextureCache.hash31("font:12:6x12:ascii")
|
|
380
|
+
XCTAssertEqual(Array(p.puts[0][21 ..< 25]), u32(fontId))
|
|
381
|
+
XCTAssertEqual(Array(p.puts[0][17 ..< 21]), u32(G2CfwTextureCache.hash31("t:ascii:ab")))
|
|
382
|
+
// the font is one asset with all 95 glyphs, uploaded once, then the two strings
|
|
383
|
+
XCTAssertFalse(encoded.uploads.isEmpty)
|
|
384
|
+
let entries = encoded.uploads.flatMap { [UInt8]($0).dropFirst(5) }
|
|
385
|
+
XCTAssertEqual(entries[0], 0)
|
|
386
|
+
XCTAssertEqual(Array(entries[1 ..< 5]), u32(fontId))
|
|
387
|
+
XCTAssertEqual(entries[9], G2CfwTextureCache.kindFont)
|
|
388
|
+
let total = Int(entries[10]) | Int(entries[11]) << 8 | Int(entries[12]) << 16
|
|
389
|
+
XCTAssertEqual(total, 96 * 4 + 95 * (2 + 2), "table + 95 glyphs of 72 white pixels (one 2-byte RLE token each)")
|
|
390
|
+
let moved = try XCTUnwrap(scene.encode(frame([element("g", type: "text", x: 0, y: 20, w: 60, h: 24, text: "ab\ncd", font: "mono", fontSize: 12, transitionMs: 330)]), originX: 32, originY: 96, inlineText: true, mono: mono, cache: cache) { _, _, _ in nil })
|
|
391
|
+
let q = parse(moved.payload)
|
|
392
|
+
XCTAssertTrue(q.puts.isEmpty)
|
|
393
|
+
XCTAssertTrue(moved.uploads.isEmpty)
|
|
394
|
+
XCTAssertEqual(Array(q.ops[5 ..< 8]), [0x03, 0x01, 10], "x, y and the options byte (a fade-capable texture)")
|
|
395
|
+
XCTAssertEqual(Array(q.ops[12 ..< 18]), le(34, 118, 15))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func testHalfBlockGridUsesASeparateFontAndNonAsciiKeepsTiles() throws {
|
|
399
|
+
let (scene, cache) = fresh()
|
|
400
|
+
let blocks = element("b", type: "text", x: 0, y: 0, w: 60, h: 12, text: "▀▄█ ", font: "mono", fontSize: 12)
|
|
401
|
+
let encoded = try XCTUnwrap(scene.encode(frame([blocks]), originX: 32, originY: 96, inlineText: true, mono: mono, cache: cache) { _, _, _ in nil })
|
|
402
|
+
let p = parse(encoded.payload)
|
|
403
|
+
XCTAssertEqual(p.puts.count, 1)
|
|
404
|
+
XCTAssertEqual(Array(p.puts[0][21 ..< 25]), u32(G2CfwTextureCache.hash31("font:12:6x12:blocks")))
|
|
405
|
+
XCTAssertEqual(Array(p.puts[0][17 ..< 21]), u32(G2CfwTextureCache.hash31("t:blocks:▀▄█ ")))
|
|
406
|
+
let mixed = element("m", type: "text", x: 0, y: 0, w: 60, h: 12, text: "a▀", font: "mono", fontSize: 12)
|
|
407
|
+
let tiles = try XCTUnwrap(scene.encode(frame([mixed]), originX: 32, originY: 96, inlineText: true, mono: mono, cache: cache) { _, w, h in Data(repeating: 0xFF, count: w * h) })
|
|
408
|
+
XCTAssertEqual(parse(tiles.payload).puts[0][9], 14)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
func testMovingBackToTheResidentDefinitionWithoutTransitionSnapsInsteadOfRedefining() throws {
|
|
412
|
+
let (scene, cache) = fresh()
|
|
413
|
+
let home = element("row", type: "rect", x: 300, y: 40, w: 200, h: 30, border: 1, hash: "r")
|
|
414
|
+
let away = element("row", type: "rect", x: 20, y: 80, w: 500, h: 30, border: 1, transitionMs: 300, hash: "r")
|
|
415
|
+
let first = try XCTUnwrap(encode(scene, cache, [home]))
|
|
416
|
+
XCTAssertEqual(first.puts, 1)
|
|
417
|
+
let moved = try XCTUnwrap(encode(scene, cache, [away]))
|
|
418
|
+
XCTAssertEqual(moved.tweens, 1, "a transition tweens the row away; its version stays")
|
|
419
|
+
// Back home with no transition: a PUT of the same version would be a
|
|
420
|
+
// firmware no-op and leave the row where the tween put it, so the
|
|
421
|
+
// encoder snaps it with a zero-frame tween instead.
|
|
422
|
+
let back = try XCTUnwrap(encode(scene, cache, [home]))
|
|
423
|
+
XCTAssertEqual(back.puts, 0)
|
|
424
|
+
XCTAssertEqual(back.hits, 1)
|
|
425
|
+
XCTAssertTrue(back.changed)
|
|
426
|
+
let p = parse(back.payload)
|
|
427
|
+
XCTAssertEqual(p.refs.map(\.1), parse(first.payload).refs.map(\.1), "same version as the original definition")
|
|
428
|
+
XCTAssertEqual(p.ops.first, 5, "a TWEEN op")
|
|
429
|
+
XCTAssertEqual(p.ops[7], 0, "of zero frames")
|
|
430
|
+
// The next identical frame is a plain hit.
|
|
431
|
+
let again = try XCTUnwrap(encode(scene, cache, [home]))
|
|
432
|
+
XCTAssertFalse(again.changed)
|
|
433
|
+
// A genuinely new definition still redefines.
|
|
434
|
+
let restyled = element("row", type: "rect", x: 300, y: 40, w: 200, h: 30, border: 3, hash: "r")
|
|
435
|
+
XCTAssertEqual(try XCTUnwrap(encode(scene, cache, [restyled])).puts, 1)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func testDepthShiftsEachLensMirrorTheOppositeWayAndFlatEncodesIgnoreIt() throws {
|
|
439
|
+
|
|
440
|
+
let cache = G2CfwTextureCache()
|
|
441
|
+
let card = element("card", type: "rect", x: 100, y: 40, w: 200, h: 100, border: 2, hash: "c", depth: 8)
|
|
442
|
+
|
|
443
|
+
let flat = element("bg", type: "rect", x: 10, y: 10, w: 50, h: 50, border: 1, hash: "b")
|
|
444
|
+
let left = G2CfwScene(), right = G2CfwScene(), mono = G2CfwScene()
|
|
445
|
+
cache.reset(generation: 1)
|
|
446
|
+
left.reset(generation: 1); right.reset(generation: 1); mono.reset(generation: 1)
|
|
447
|
+
let l = parse(try XCTUnwrap(encode(left, cache, [flat, card], parallax: 1)).payload)
|
|
448
|
+
let r = parse(try XCTUnwrap(encode(right, cache, [flat, card], parallax: -1)).payload)
|
|
449
|
+
let m = parse(try XCTUnwrap(encode(mono, cache, [flat, card])).payload)
|
|
450
|
+
// A put is [1][id u32][version u32] + the record [type][flags][color][width][x i16][y i16]…
|
|
451
|
+
func x(_ put: [UInt8]) -> Int { Int(Int16(bitPattern: UInt16(put[9 + 4]) | UInt16(put[9 + 5]) << 8)) }
|
|
452
|
+
XCTAssertEqual(x(l.puts[0]), 32 + 10, "flat elements sit at the window origin on every lens")
|
|
453
|
+
XCTAssertEqual(x(r.puts[0]), 32 + 10)
|
|
454
|
+
XCTAssertEqual(x(l.puts[1]), 32 + 100 + 8, "the left lens draws a near element further right")
|
|
455
|
+
XCTAssertEqual(x(r.puts[1]), 32 + 100 - 8, "and the right lens further left")
|
|
456
|
+
XCTAssertEqual(x(m.puts[1]), 32 + 100, "a flat encode ignores depth")
|
|
457
|
+
XCTAssertEqual(l.puts[0], r.puts[0], "the flat element's definition is identical on both lenses")
|
|
458
|
+
XCTAssertNotEqual(l.puts[1], r.puts[1])
|
|
459
|
+
XCTAssertEqual(l.refs[1].0, r.refs[1].0, "same object id on both lenses, different versions")
|
|
460
|
+
XCTAssertNotEqual(l.refs[1].1, r.refs[1].1)
|
|
461
|
+
|
|
462
|
+
// Bringing the card to the frame's plane with a transition tweens x on each lens, in opposite directions.
|
|
463
|
+
let level = element("card", type: "rect", x: 100, y: 40, w: 200, h: 100, border: 2, transitionMs: 330, hash: "c")
|
|
464
|
+
let l2 = try XCTUnwrap(encode(left, cache, [flat, level], parallax: 1))
|
|
465
|
+
let r2 = try XCTUnwrap(encode(right, cache, [flat, level], parallax: -1))
|
|
466
|
+
XCTAssertEqual(l2.tweens, 1)
|
|
467
|
+
XCTAssertEqual(r2.tweens, 1)
|
|
468
|
+
XCTAssertNotEqual(l2.payload, r2.payload)
|
|
469
|
+
XCTAssertEqual(parse(l2.payload).puts.count, 0)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
func testAdoptCopiesTheMirrorSoTheSecondLensStartsFromSharedResidency() throws {
|
|
473
|
+
let cache = G2CfwTextureCache()
|
|
474
|
+
let flat = element("bg", type: "rect", x: 10, y: 10, w: 50, h: 50, border: 1, hash: "b")
|
|
475
|
+
let left = G2CfwScene(), right = G2CfwScene()
|
|
476
|
+
cache.reset(generation: 3)
|
|
477
|
+
left.reset(generation: 3)
|
|
478
|
+
_ = try XCTUnwrap(encode(left, cache, [flat]))
|
|
479
|
+
|
|
480
|
+
right.adopt(left)
|
|
481
|
+
XCTAssertEqual(right.generation, 3)
|
|
482
|
+
XCTAssertEqual(right.active, left.active)
|
|
483
|
+
let again = try XCTUnwrap(encode(right, cache, [flat], parallax: -1))
|
|
484
|
+
XCTAssertEqual(again.puts, 0, "what the left lens holds, the right holds too")
|
|
485
|
+
XCTAssertEqual(again.hits, 1)
|
|
486
|
+
XCTAssertFalse(again.changed)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func testOnlyNonAnimatedImageFramesPreferRaster() {
|
|
490
|
+
|
|
491
|
+
let image = element("i", type: "image", w: 10, h: 10)
|
|
492
|
+
XCTAssertTrue(G2CfwScene.prefersRaster(frame([image])))
|
|
493
|
+
XCTAssertFalse(G2CfwScene.prefersRaster(frame([image, element("c", type: "circle", w: 5, h: 5)])))
|
|
494
|
+
XCTAssertFalse(G2CfwScene.prefersRaster(frame([element("i", type: "image", w: 10, h: 10, transitionMs: 100)])))
|
|
495
|
+
XCTAssertFalse(G2CfwScene.prefersRaster(frame([])))
|
|
496
|
+
}
|
|
497
|
+
}
|