@unrulysystems/native-motion 0.1.0-alpha.0
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// U7f dynamic-drag-config device checks: catalog _conditionalDraggable +
|
|
2
|
+
// _dragConstraintChanges on the public MotionView path. iOS has no tap injection, so
|
|
3
|
+
// the screen self-drives. Discriminators are the freeze/refusal repeal (the catalog
|
|
4
|
+
// blockers): toggling drag and rebuilding numeric constraints must not refuse, and
|
|
5
|
+
// bare dragElastic without constraints must not refuse. Pan follow is unit-proven on
|
|
6
|
+
// the MotionView host path (dynamicDragConfig.host.test.ts).
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState } from 'react'
|
|
9
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
10
|
+
import { MotionView } from '../../native/motion/MotionView'
|
|
11
|
+
import { MotionRoot } from '../../native/motion/MotionRoot'
|
|
12
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
13
|
+
|
|
14
|
+
const ROW_FALSE = 'drag={false} mounts without a typed refusal'
|
|
15
|
+
const ROW_TOGGLE = 'toggling drag true/false does not freeze-refuse (catalog _conditionalDraggable)'
|
|
16
|
+
const ROW_CONSTRAINTS = 'rebuilding numeric constraints does not freeze-refuse'
|
|
17
|
+
const ROW_ELASTIC = 'bare dragElastic without constraints is a no-op (no refusal)'
|
|
18
|
+
const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
|
|
19
|
+
const ROWS = [ROW_FALSE, ROW_TOGGLE, ROW_CONSTRAINTS, ROW_ELASTIC, ROW_CLEAN] as const
|
|
20
|
+
|
|
21
|
+
const T_FALSE = 400
|
|
22
|
+
const T_TOGGLE_ON = 900
|
|
23
|
+
const T_TOGGLE_OFF = 1400
|
|
24
|
+
const T_CONSTRAINTS = 1900
|
|
25
|
+
const T_CONSTRAINTS_SAMPLE = 2300
|
|
26
|
+
const T_FINISH = 2800
|
|
27
|
+
|
|
28
|
+
interface RowResult {
|
|
29
|
+
readonly pass: boolean
|
|
30
|
+
readonly detail: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function DynamicDragConfigChecksScreen() {
|
|
34
|
+
const severity = ambientNativeSeverity()
|
|
35
|
+
const [dragOn, setDragOn] = useState(false)
|
|
36
|
+
const [bound, setBound] = useState(200)
|
|
37
|
+
const [stage, setStage] = useState('boot')
|
|
38
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
|
|
39
|
+
const reportsRef = useRef<string[]>([])
|
|
40
|
+
|
|
41
|
+
const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
|
|
42
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const originalError = console.error
|
|
47
|
+
console.error = (...args: unknown[]) => {
|
|
48
|
+
for (const arg of args) {
|
|
49
|
+
const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
|
|
50
|
+
if (message !== null && /motion|drag/i.test(message)) reportsRef.current.push(message)
|
|
51
|
+
}
|
|
52
|
+
originalError(...args)
|
|
53
|
+
}
|
|
54
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
55
|
+
const at = (ms: number, run: () => void): void => {
|
|
56
|
+
timers.push(setTimeout(run, ms))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
at(T_FALSE, () => {
|
|
60
|
+
const reports = reportsRef.current.length
|
|
61
|
+
finalize(
|
|
62
|
+
ROW_FALSE,
|
|
63
|
+
reports === 0,
|
|
64
|
+
reports === 0
|
|
65
|
+
? 'drag={false} mounted with zero motion/drag reports'
|
|
66
|
+
: `${reports} report(s) at disabled mount`,
|
|
67
|
+
)
|
|
68
|
+
setStage('disabled mount sampled')
|
|
69
|
+
})
|
|
70
|
+
at(T_TOGGLE_ON, () => {
|
|
71
|
+
setDragOn(true)
|
|
72
|
+
setStage('toggled drag on')
|
|
73
|
+
})
|
|
74
|
+
at(T_TOGGLE_OFF, () => {
|
|
75
|
+
setDragOn(false)
|
|
76
|
+
const freeze = reportsRef.current.some((message) => /fixed at mount/i.test(message))
|
|
77
|
+
finalize(
|
|
78
|
+
ROW_TOGGLE,
|
|
79
|
+
!freeze,
|
|
80
|
+
freeze
|
|
81
|
+
? 'freeze-refusal fired on toggle (the pre-U7f mount-frozen law)'
|
|
82
|
+
: 'toggle true→false with no fixed-at-mount refusal',
|
|
83
|
+
)
|
|
84
|
+
setStage('toggled drag off')
|
|
85
|
+
})
|
|
86
|
+
at(T_CONSTRAINTS, () => {
|
|
87
|
+
setBound(20)
|
|
88
|
+
setStage('constraints rebuilt')
|
|
89
|
+
})
|
|
90
|
+
at(T_CONSTRAINTS_SAMPLE, () => {
|
|
91
|
+
const freeze = reportsRef.current.some((message) => /fixed at mount/i.test(message))
|
|
92
|
+
const elastic = reportsRef.current.some((message) => /meaningless/i.test(message))
|
|
93
|
+
finalize(
|
|
94
|
+
ROW_CONSTRAINTS,
|
|
95
|
+
!freeze,
|
|
96
|
+
freeze
|
|
97
|
+
? 'freeze-refusal fired on constraint rebuild'
|
|
98
|
+
: 'constraint bound 200→20 with no fixed-at-mount refusal',
|
|
99
|
+
)
|
|
100
|
+
finalize(
|
|
101
|
+
ROW_ELASTIC,
|
|
102
|
+
!elastic,
|
|
103
|
+
elastic
|
|
104
|
+
? 'dragElastic-without-constraints still refused'
|
|
105
|
+
: 'bare dragElastic peer mounted with no meaningless-elastic refusal',
|
|
106
|
+
)
|
|
107
|
+
setStage('constraint rebuild sampled')
|
|
108
|
+
})
|
|
109
|
+
at(T_FINISH, () => {
|
|
110
|
+
const reports = reportsRef.current
|
|
111
|
+
finalize(
|
|
112
|
+
ROW_CLEAN,
|
|
113
|
+
reports.length === 0,
|
|
114
|
+
reports.length === 0
|
|
115
|
+
? '0 refusal reports across disabled mount, toggle, constraint rebuild, elastic peer'
|
|
116
|
+
: `${reports.length} report(s): ${reports.map((message) => message.slice(0, 80)).join(' | ')}`,
|
|
117
|
+
)
|
|
118
|
+
setStage('done')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
for (const timer of timers) clearTimeout(timer)
|
|
123
|
+
console.error = originalError
|
|
124
|
+
}
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
const done = ROWS.every((id) => rows[id] !== undefined)
|
|
128
|
+
const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
129
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
130
|
+
const passed = Object.keys(rows).length - failing.length
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<MotionRoot>
|
|
134
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
135
|
+
<Text style={styles.title}>U7f dynamic-drag-config checks</Text>
|
|
136
|
+
<Text style={styles.subtitle}>
|
|
137
|
+
catalog toggle + live constraints + elastic no-op ({severity} severity)
|
|
138
|
+
</Text>
|
|
139
|
+
<View
|
|
140
|
+
style={[
|
|
141
|
+
styles.banner,
|
|
142
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
146
|
+
</View>
|
|
147
|
+
<Text style={styles.tally}>
|
|
148
|
+
{done
|
|
149
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
|
|
150
|
+
: `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
|
|
151
|
+
</Text>
|
|
152
|
+
<Text style={styles.tallyDetail}>
|
|
153
|
+
{done
|
|
154
|
+
? failing.length === 0
|
|
155
|
+
? 'no failing rows'
|
|
156
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
157
|
+
: `stage: ${stage}`}
|
|
158
|
+
</Text>
|
|
159
|
+
<Text style={styles.status}>
|
|
160
|
+
drag={dragOn ? 'true' : 'false'} · bound={bound}
|
|
161
|
+
</Text>
|
|
162
|
+
|
|
163
|
+
{dragOn ? (
|
|
164
|
+
<MotionView drag style={[styles.box, { backgroundColor: '#16a34a' }]} />
|
|
165
|
+
) : (
|
|
166
|
+
<MotionView drag={false} style={[styles.box, { backgroundColor: '#eab308' }]} />
|
|
167
|
+
)}
|
|
168
|
+
<MotionView
|
|
169
|
+
drag="x"
|
|
170
|
+
dragConstraints={{ left: 0, right: bound }}
|
|
171
|
+
dragElastic={0}
|
|
172
|
+
style={[styles.box, { backgroundColor: '#3b82f6' }]}
|
|
173
|
+
/>
|
|
174
|
+
<MotionView drag="x" dragElastic style={[styles.box, { backgroundColor: '#f97316' }]} />
|
|
175
|
+
|
|
176
|
+
{ROWS.map((id) => {
|
|
177
|
+
const row = rows[id]
|
|
178
|
+
return (
|
|
179
|
+
<View key={id} style={styles.row}>
|
|
180
|
+
<Text
|
|
181
|
+
style={[
|
|
182
|
+
styles.verdict,
|
|
183
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
184
|
+
]}
|
|
185
|
+
>
|
|
186
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
187
|
+
</Text>
|
|
188
|
+
<View style={styles.rowBody}>
|
|
189
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
190
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
191
|
+
{row === undefined ? 'pending' : row.detail}
|
|
192
|
+
</Text>
|
|
193
|
+
</View>
|
|
194
|
+
</View>
|
|
195
|
+
)
|
|
196
|
+
})}
|
|
197
|
+
</ScrollView>
|
|
198
|
+
</MotionRoot>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const styles = StyleSheet.create({
|
|
203
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
204
|
+
container: { paddingVertical: 72, paddingHorizontal: 8 },
|
|
205
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
206
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
207
|
+
banner: {
|
|
208
|
+
alignSelf: 'center',
|
|
209
|
+
paddingHorizontal: 36,
|
|
210
|
+
paddingVertical: 10,
|
|
211
|
+
borderRadius: 10,
|
|
212
|
+
marginTop: 18,
|
|
213
|
+
marginBottom: 10,
|
|
214
|
+
},
|
|
215
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
216
|
+
fail: { backgroundColor: '#a32633' },
|
|
217
|
+
running: { backgroundColor: '#7a641d' },
|
|
218
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
219
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
220
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
221
|
+
status: {
|
|
222
|
+
color: '#c9c9d8',
|
|
223
|
+
fontSize: 11,
|
|
224
|
+
marginTop: 8,
|
|
225
|
+
textAlign: 'center',
|
|
226
|
+
fontFamily: 'Menlo',
|
|
227
|
+
},
|
|
228
|
+
box: { width: 72, height: 72, marginTop: 12, alignSelf: 'center', borderRadius: 8 },
|
|
229
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
230
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
231
|
+
ok: { color: '#4ade80' },
|
|
232
|
+
bad: { color: '#f87171' },
|
|
233
|
+
pending: { color: '#8a8aa0' },
|
|
234
|
+
rowBody: { flex: 1 },
|
|
235
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
236
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
237
|
+
})
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// U7j frameData device checks: catalog useVelocity.tsx on the public path. iOS has no
|
|
2
|
+
// tap injection, so the screen self-drives. Discriminators (Decision 90 analog of 77/82/86):
|
|
3
|
+
// frameData is the JS-thread clock (F1 — not the UI driver). Host paint of x is the web e2e.
|
|
4
|
+
// Native stamps frameData.timestamp while a JS-channel animate() of a MotionValue runs, then
|
|
5
|
+
// proves idle freeze. The `frame` scheduler is not this unit.
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
8
|
+
import { StyleSheet, Text, View } from 'react-native'
|
|
9
|
+
import { MotionRoot } from '../../native/motion/MotionRoot'
|
|
10
|
+
import { animate } from '../../native/motion/imperativeAnimate'
|
|
11
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
12
|
+
import { frameData } from '../../native/motion/frameData'
|
|
13
|
+
import { useMotionValue, useMotionValueEvent, useVelocity } from '../../native/motion/valueHooks'
|
|
14
|
+
import { armProofConsoleTap } from './proofConsoleTap'
|
|
15
|
+
|
|
16
|
+
const ROW_SHAPE = 'frameData shape: delta, timestamp, isProcessing'
|
|
17
|
+
const ROW_ADVANCE = 'timestamp advances while the JS channel is kicked'
|
|
18
|
+
const ROW_FREEZE = 'idle freeze: two reads after settle are identical'
|
|
19
|
+
const ROW_CLEAN = 'clean run: zero unexpected reports'
|
|
20
|
+
const ROWS = [ROW_SHAPE, ROW_ADVANCE, ROW_FREEZE, ROW_CLEAN] as const
|
|
21
|
+
|
|
22
|
+
const T_SHAPE = 200
|
|
23
|
+
const T_START = 400
|
|
24
|
+
const T_MID = 700
|
|
25
|
+
const T_IDLE_A = 1400
|
|
26
|
+
const T_IDLE_B = 1800
|
|
27
|
+
const T_CLEAN = 2100
|
|
28
|
+
const DURATION_S = 0.45
|
|
29
|
+
|
|
30
|
+
interface RowResult {
|
|
31
|
+
readonly pass: boolean
|
|
32
|
+
readonly detail: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function FrameDataChecksScreen() {
|
|
36
|
+
const severity = ambientNativeSeverity()
|
|
37
|
+
const x = useMotionValue(0)
|
|
38
|
+
const xVelocity = useVelocity(x)
|
|
39
|
+
const [stage, setStage] = useState('boot')
|
|
40
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
|
|
41
|
+
const [xNow, setXNow] = useState(0)
|
|
42
|
+
const [stampNow, setStampNow] = useState(() => frameData.timestamp)
|
|
43
|
+
const stampsRef = useRef<number[]>([])
|
|
44
|
+
const idleARef = useRef<number | null>(null)
|
|
45
|
+
const reportsRef = useRef<string[]>([])
|
|
46
|
+
const restoreTapRef = useRef<null | (() => void)>(null)
|
|
47
|
+
if (restoreTapRef.current === null) {
|
|
48
|
+
restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
|
|
49
|
+
/motion|frameData|animate|velocity/i.test(message),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
|
|
54
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onX = useCallback((value: number): void => {
|
|
58
|
+
setXNow(value)
|
|
59
|
+
const stamp = frameData.timestamp
|
|
60
|
+
stampsRef.current.push(stamp)
|
|
61
|
+
setStampNow(stamp)
|
|
62
|
+
}, [])
|
|
63
|
+
useMotionValueEvent(x, 'change', onX)
|
|
64
|
+
useMotionValueEvent(xVelocity, 'change', () => {
|
|
65
|
+
setStampNow(frameData.timestamp)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
70
|
+
const at = (ms: number, run: () => void): void => {
|
|
71
|
+
timers.push(setTimeout(run, ms))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
at(T_SHAPE, () => {
|
|
75
|
+
const pass =
|
|
76
|
+
typeof frameData.delta === 'number' &&
|
|
77
|
+
typeof frameData.timestamp === 'number' &&
|
|
78
|
+
typeof frameData.isProcessing === 'boolean'
|
|
79
|
+
finalize(
|
|
80
|
+
ROW_SHAPE,
|
|
81
|
+
pass,
|
|
82
|
+
pass
|
|
83
|
+
? `delta=${frameData.delta.toFixed(2)} ts=${frameData.timestamp.toFixed(1)} processing=${frameData.isProcessing}`
|
|
84
|
+
: 'shape missing',
|
|
85
|
+
)
|
|
86
|
+
setStage('shape sampled')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
at(T_START, () => {
|
|
90
|
+
animate(x, 100, { duration: DURATION_S, ease: 'linear' })
|
|
91
|
+
setStage('js-channel animate started')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
at(T_MID, () => {
|
|
95
|
+
const stamps = stampsRef.current
|
|
96
|
+
const pass = stamps.length >= 2 && stamps[stamps.length - 1]! > stamps[0]!
|
|
97
|
+
finalize(
|
|
98
|
+
ROW_ADVANCE,
|
|
99
|
+
pass,
|
|
100
|
+
pass
|
|
101
|
+
? `stamps=${stamps.length} first=${stamps[0]!.toFixed(1)} last=${stamps[stamps.length - 1]!.toFixed(1)}`
|
|
102
|
+
: `stamps=${stamps.length} — expected advancing timestamps`,
|
|
103
|
+
)
|
|
104
|
+
setStage(pass ? 'advance row finalized' : 'advance row finalized (FAIL)')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
at(T_IDLE_A, () => {
|
|
108
|
+
idleARef.current = frameData.timestamp
|
|
109
|
+
setStage(`idle-a ts=${frameData.timestamp.toFixed(1)}`)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
at(T_IDLE_B, () => {
|
|
113
|
+
const a = idleARef.current
|
|
114
|
+
const b = frameData.timestamp
|
|
115
|
+
const pass = a !== null && a === b
|
|
116
|
+
finalize(
|
|
117
|
+
ROW_FREEZE,
|
|
118
|
+
pass,
|
|
119
|
+
pass
|
|
120
|
+
? `idle ${a!.toFixed(1)} === ${b.toFixed(1)}`
|
|
121
|
+
: `idle-a=${a === null ? 'n/a' : a.toFixed(1)} idle-b=${b.toFixed(1)}`,
|
|
122
|
+
)
|
|
123
|
+
setStage(pass ? 'freeze row finalized' : 'freeze row finalized (FAIL)')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
at(T_CLEAN, () => {
|
|
127
|
+
finalize(
|
|
128
|
+
ROW_CLEAN,
|
|
129
|
+
reportsRef.current.length === 0,
|
|
130
|
+
reportsRef.current.length === 0
|
|
131
|
+
? '0 unexpected reports'
|
|
132
|
+
: `${reportsRef.current.length} unexpected: ${reportsRef.current
|
|
133
|
+
.map((message) => message.slice(0, 80))
|
|
134
|
+
.join(' | ')}`,
|
|
135
|
+
)
|
|
136
|
+
setStage('done')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
for (const timer of timers) clearTimeout(timer)
|
|
141
|
+
restoreTapRef.current?.()
|
|
142
|
+
restoreTapRef.current = null
|
|
143
|
+
}
|
|
144
|
+
}, [x])
|
|
145
|
+
|
|
146
|
+
const done = ROWS.every((id) => rows[id] !== undefined)
|
|
147
|
+
const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
148
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
149
|
+
const passed = Object.keys(rows).length - failing.length
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<MotionRoot>
|
|
153
|
+
<View style={styles.scroll}>
|
|
154
|
+
<View style={styles.container}>
|
|
155
|
+
<Text style={styles.title}>U7j frameData checks</Text>
|
|
156
|
+
<Text style={styles.subtitle}>
|
|
157
|
+
catalog useVelocity timestamps + JS-channel kick ({severity} severity)
|
|
158
|
+
</Text>
|
|
159
|
+
<View
|
|
160
|
+
style={[
|
|
161
|
+
styles.banner,
|
|
162
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
163
|
+
]}
|
|
164
|
+
>
|
|
165
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
166
|
+
</View>
|
|
167
|
+
<Text style={styles.tally}>
|
|
168
|
+
{done
|
|
169
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
|
|
170
|
+
: `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
|
|
171
|
+
</Text>
|
|
172
|
+
<Text style={styles.tallyDetail}>
|
|
173
|
+
{done
|
|
174
|
+
? failing.length === 0
|
|
175
|
+
? 'no failing rows'
|
|
176
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
177
|
+
: `stage: ${stage}`}
|
|
178
|
+
</Text>
|
|
179
|
+
<Text style={styles.status}>
|
|
180
|
+
x={xNow.toFixed(1)} · ts={stampNow.toFixed(1)} · {severity}
|
|
181
|
+
</Text>
|
|
182
|
+
<View style={styles.box} accessibilityLabel="u7j-frame-data-box" />
|
|
183
|
+
|
|
184
|
+
{ROWS.map((id) => {
|
|
185
|
+
const row = rows[id]
|
|
186
|
+
return (
|
|
187
|
+
<View key={id} style={styles.row}>
|
|
188
|
+
<Text
|
|
189
|
+
style={[
|
|
190
|
+
styles.verdict,
|
|
191
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
192
|
+
]}
|
|
193
|
+
>
|
|
194
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
195
|
+
</Text>
|
|
196
|
+
<View style={styles.rowBody}>
|
|
197
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
198
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
199
|
+
{row === undefined ? 'pending' : row.detail}
|
|
200
|
+
</Text>
|
|
201
|
+
</View>
|
|
202
|
+
</View>
|
|
203
|
+
)
|
|
204
|
+
})}
|
|
205
|
+
</View>
|
|
206
|
+
</View>
|
|
207
|
+
</MotionRoot>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const styles = StyleSheet.create({
|
|
212
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
213
|
+
container: { paddingVertical: 72, paddingHorizontal: 8 },
|
|
214
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
215
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
216
|
+
banner: {
|
|
217
|
+
alignSelf: 'center',
|
|
218
|
+
paddingHorizontal: 36,
|
|
219
|
+
paddingVertical: 10,
|
|
220
|
+
borderRadius: 10,
|
|
221
|
+
marginTop: 18,
|
|
222
|
+
marginBottom: 10,
|
|
223
|
+
},
|
|
224
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
225
|
+
fail: { backgroundColor: '#a32633' },
|
|
226
|
+
running: { backgroundColor: '#7a641d' },
|
|
227
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
228
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
229
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
230
|
+
status: {
|
|
231
|
+
color: '#c9c9d8',
|
|
232
|
+
fontSize: 11,
|
|
233
|
+
marginTop: 8,
|
|
234
|
+
textAlign: 'center',
|
|
235
|
+
fontFamily: 'Menlo',
|
|
236
|
+
},
|
|
237
|
+
box: {
|
|
238
|
+
width: 100,
|
|
239
|
+
height: 100,
|
|
240
|
+
marginTop: 14,
|
|
241
|
+
backgroundColor: '#ef4444',
|
|
242
|
+
borderRadius: 8,
|
|
243
|
+
},
|
|
244
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
245
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
246
|
+
ok: { color: '#4ade80' },
|
|
247
|
+
bad: { color: '#f87171' },
|
|
248
|
+
pending: { color: '#8a8aa0' },
|
|
249
|
+
rowBody: { flex: 1 },
|
|
250
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
251
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
252
|
+
})
|