@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.
Files changed (155) hide show
  1. package/README.md +47 -6
  2. package/android/build.gradle +7 -7
  3. package/android/lc3Lib/build.gradle +2 -2
  4. package/android/src/main/assets/G2Firmware/README.md +27 -2
  5. 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
  6. 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
  7. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
  8. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
  9. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
  10. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
  11. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
  12. package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
  13. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
  14. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
  15. package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
  16. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1075 -257
  17. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
  18. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
  19. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
  20. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
  21. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
  22. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
  23. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
  24. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  25. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
  26. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
  27. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
  28. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
  29. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
  30. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
  31. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
  32. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
  33. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
  34. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
  35. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
  36. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
  37. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
  38. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
  39. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
  40. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
  41. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
  42. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
  43. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
  44. package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
  45. package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
  46. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
  47. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
  48. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
  49. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
  50. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
  51. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
  52. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
  53. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
  54. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
  55. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
  56. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
  57. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
  58. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
  59. package/build/BluetoothSdk.types.d.ts +209 -20
  60. package/build/BluetoothSdk.types.d.ts.map +1 -1
  61. package/build/BluetoothSdk.types.js.map +1 -1
  62. package/build/_internal.d.ts +1 -1
  63. package/build/_internal.d.ts.map +1 -1
  64. package/build/_internal.js.map +1 -1
  65. package/build/_private/BluetoothSdkModule.d.ts +30 -1
  66. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  67. package/build/_private/BluetoothSdkModule.js.map +1 -1
  68. package/build/generated/changelogCatalog.d.ts +28 -1
  69. package/build/generated/changelogCatalog.d.ts.map +1 -1
  70. package/build/generated/changelogCatalog.js +37 -1
  71. package/build/generated/changelogCatalog.js.map +1 -1
  72. package/build/generated/releaseMetadata.js +4 -4
  73. package/build/generated/releaseMetadata.js.map +1 -1
  74. package/build/index.d.ts +1 -1
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +14 -0
  77. package/build/index.js.map +1 -1
  78. package/ios/BluetoothSdkModule.swift +102 -13
  79. package/ios/GlasslyBluetoothSDK.podspec +1 -1
  80. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
  81. package/ios/Source/BluetoothSDK.swift +53 -0
  82. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  83. package/ios/Source/Bridge.swift +13 -0
  84. package/ios/Source/DeviceManager.swift +151 -33
  85. package/ios/Source/DeviceStore.swift +28 -14
  86. package/ios/Source/DisplayPacer.swift +180 -0
  87. package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
  88. package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
  89. package/ios/Source/GlassesNotifications.swift +181 -0
  90. package/ios/Source/ObservableStore.swift +14 -2
  91. package/ios/Source/Resources/G2Firmware/README.md +27 -2
  92. package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  93. package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  94. package/ios/Source/controllers/R1.swift +0 -1
  95. package/ios/Source/internal/BluetoothAvailability.swift +31 -0
  96. package/ios/Source/sgcs/G2.swift +1434 -504
  97. package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
  98. package/ios/Source/sgcs/G2BleLink.swift +102 -0
  99. package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
  100. package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
  101. package/ios/Source/sgcs/G2CfwScene.swift +779 -0
  102. package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
  103. package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
  104. package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
  105. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  106. package/ios/Source/sgcs/G2Flasher.swift +9 -9
  107. package/ios/Source/sgcs/G2Keyboard.swift +780 -0
  108. package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
  109. package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
  110. package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
  111. package/ios/Source/sgcs/G2RingBattery.swift +22 -0
  112. package/ios/Source/sgcs/G2Sensors.swift +75 -0
  113. package/ios/Source/sgcs/G2TouchInput.swift +51 -0
  114. package/ios/Source/sgcs/G2WritePacing.swift +18 -0
  115. package/ios/Source/sgcs/MentraNex.swift +44 -44
  116. package/ios/Source/sgcs/SGCManager.swift +120 -13
  117. package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
  118. package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
  119. package/ios/Source/status/DeviceStatus.swift +27 -0
  120. package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
  121. package/ios/Source/utils/Enums.swift +0 -1
  122. package/ios/Source/utils/G1Text.swift +0 -1
  123. package/ios/Source/utils/G2NotifyFile.swift +20 -10
  124. package/ios/Source/utils/JSCExperiment.swift +6 -6
  125. package/ios/Source/utils/Models.swift +0 -1
  126. package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
  127. package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
  128. package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
  129. package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
  130. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
  131. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
  132. package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
  133. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
  134. package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
  135. package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
  136. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
  137. package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
  138. package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
  139. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
  140. package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
  141. package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
  142. package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
  143. package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
  144. package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
  145. package/package.json +1 -1
  146. package/plugin/build/withAndroid.js +2 -2
  147. package/plugin/build/withIos.js +1 -1
  148. package/scripts/gen-nex-proto.sh +12 -0
  149. package/scripts/inject-ios-sdk-version.mjs +1 -1
  150. package/src/BluetoothSdk.types.ts +214 -19
  151. package/src/_internal.ts +1 -1
  152. package/src/_private/BluetoothSdkModule.ts +40 -0
  153. package/src/generated/changelogCatalog.ts +37 -1
  154. package/src/generated/releaseMetadata.ts +4 -4
  155. 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
+ }