1- import { useCallback , useMemo , useState } from 'react' ;
1+ import { ReactNode , useCallback , useMemo , useState } from 'react' ;
22import { AssignTrackerRequestT , BodyPart , RpcMessage } from 'solarxr-protocol' ;
33import { useOnboarding } from '@/hooks/onboarding' ;
44import { useWebsocketAPI } from '@/hooks/websocket-api' ;
@@ -12,7 +12,7 @@ import { TipBox } from '@/components/commons/TipBox';
1212import { Typography } from '@/components/commons/Typography' ;
1313import { BodyAssignment } from '@/components/onboarding/BodyAssignment' ;
1414import { MountingSelectionMenu } from './MountingSelectionMenu' ;
15- import { useLocalization } from '@fluent/react' ;
15+ import { Localized } from '@fluent/react' ;
1616import { useBreakpoint } from '@/hooks/breakpoint' ;
1717import { Quaternion } from 'three' ;
1818import { AssignMode , defaultConfig , useConfig } from '@/hooks/config' ;
@@ -22,7 +22,6 @@ import * as Sentry from '@sentry/react';
2222
2323export function ManualMountingPage ( ) {
2424 const { isMobile } = useBreakpoint ( 'mobile' ) ;
25- const { l10n } = useLocalization ( ) ;
2625 const { applyProgress, state } = useOnboarding ( ) ;
2726 const { sendRPCPacket } = useWebsocketAPI ( ) ;
2827 const { config } = useConfig ( ) ;
@@ -103,28 +102,26 @@ export function ManualMountingPage() {
103102 < div className = "flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto" >
104103 < div className = "flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center" >
105104 < div className = "flex flex-col w-full xs:max-w-sm gap-3" >
106- < Typography variant = "main-title" >
107- { l10n . getString ( ' onboarding-manual_mounting' ) }
108- </ Typography >
109- < Typography >
110- { l10n . getString ( 'onboarding-manual_mounting-description' ) }
111- </ Typography >
112- < TipBox > { l10n . getString ( 'tips-find_tracker' ) } </ TipBox >
105+ < Typography variant = "main-title" id = "onboarding-manual_mounting" / >
106+ < Typography id = " onboarding-manual_mounting-description" />
107+ < Typography id = "tips-find_tracker" / >
108+ < Localized id = "tips-find_tracker" >
109+ < TipBox />
110+ </ Localized >
111+
113112 < div className = "flex flex-row gap-3 mt-auto" >
114113 < Button
115114 variant = "secondary"
116115 to = "/onboarding/mounting/choose"
117116 state = { state }
118- >
119- { l10n . getString ( 'onboarding-previous_step' ) }
120- </ Button >
117+ id = "onboarding-previous_step"
118+ />
121119 { ! state . alonePage && (
122120 < Button
123121 variant = "primary"
124122 to = "/onboarding/body-proportions/scaled"
125- >
126- { l10n . getString ( 'onboarding-manual_mounting-next' ) }
127- </ Button >
123+ id = "onboarding-manual_mounting-next"
124+ />
128125 ) }
129126 </ div >
130127 </ div >
@@ -142,3 +139,109 @@ export function ManualMountingPage() {
142139 </ >
143140 ) ;
144141}
142+
143+ export function ManualMountingPageStayAligned ( {
144+ children,
145+ } : {
146+ children : ReactNode ;
147+ } ) {
148+ const { isMobile } = useBreakpoint ( 'mobile' ) ;
149+ const { sendRPCPacket } = useWebsocketAPI ( ) ;
150+ const { config } = useConfig ( ) ;
151+
152+ const [ selectedRole , setSelectRole ] = useState < BodyPart > ( BodyPart . NONE ) ;
153+
154+ const assignedTrackers = useAtomValue ( assignedTrackersAtom ) ;
155+
156+ const trackerPartGrouped = useMemo (
157+ ( ) =>
158+ assignedTrackers . reduce < { [ key : number ] : FlatDeviceTracker [ ] } > (
159+ ( curr , td ) => {
160+ const key = td . tracker . info ?. bodyPart || BodyPart . NONE ;
161+ return {
162+ ...curr ,
163+ [ key ] : [ ...( curr [ key ] || [ ] ) , td ] ,
164+ } ;
165+ } ,
166+ { }
167+ ) ,
168+ [ assignedTrackers ]
169+ ) ;
170+
171+ const onDirectionSelected = ( mountingOrientationDegrees : Quaternion ) => {
172+ ( trackerPartGrouped [ selectedRole ] || [ ] ) . forEach ( ( td ) => {
173+ const assignreq = new AssignTrackerRequestT ( ) ;
174+
175+ assignreq . bodyPosition = td . tracker . info ?. bodyPart || BodyPart . NONE ;
176+ assignreq . mountingOrientation = MountingOrientationDegreesToQuatT (
177+ mountingOrientationDegrees
178+ ) ;
179+ assignreq . trackerId = td . tracker . trackerId ;
180+ assignreq . allowDriftCompensation = false ;
181+
182+ sendRPCPacket ( RpcMessage . AssignTrackerRequest , assignreq ) ;
183+ Sentry . metrics . count ( 'manual_mounting_set' , 1 , {
184+ attributes : {
185+ part : BodyPart [ assignreq . bodyPosition ] ,
186+ direction : assignreq . mountingOrientation ,
187+ } ,
188+ } ) ;
189+ } ) ;
190+
191+ setSelectRole ( BodyPart . NONE ) ;
192+ } ;
193+
194+ const getCurrRotation = useCallback (
195+ ( role : BodyPart ) => {
196+ if ( role === BodyPart . NONE ) return undefined ;
197+
198+ const trackers = trackerPartGrouped [ role ] || [ ] ;
199+ const [ mountingOrientation , ...orientation ] = trackers
200+ . map ( ( td ) => td . tracker . info ?. mountingOrientation )
201+ . filter ( ( orientation ) => ! ! orientation )
202+ . map ( ( orientation ) => QuaternionFromQuatT ( orientation ) ) ;
203+
204+ const identicalOrientations =
205+ mountingOrientation !== undefined &&
206+ orientation . every ( ( quat ) =>
207+ similarQuaternions ( quat , mountingOrientation )
208+ ) ;
209+ return identicalOrientations ? mountingOrientation : undefined ;
210+ } ,
211+ [ trackerPartGrouped ]
212+ ) ;
213+
214+ return (
215+ < >
216+ < MountingSelectionMenu
217+ bodyPart = { selectedRole }
218+ currRotation = { getCurrRotation ( selectedRole ) }
219+ isOpen = { selectedRole !== BodyPart . NONE }
220+ onClose = { ( ) => setSelectRole ( BodyPart . NONE ) }
221+ onDirectionSelected = { onDirectionSelected }
222+ />
223+ < div className = "flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto" >
224+ < div className = "flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center" >
225+ < div className = "flex flex-col w-full xs:max-w-sm gap-3" >
226+ < Typography variant = "main-title" id = "onboarding-manual_mounting" />
227+ < Typography id = "onboarding-manual_mounting-description" />
228+ < Typography id = "tips-find_tracker" />
229+ < Localized id = "tips-find_tracker" >
230+ < TipBox />
231+ </ Localized >
232+ { children }
233+ </ div >
234+ < div className = "flex flex-row justify-center" >
235+ < BodyAssignment
236+ width = { isMobile ? 160 : undefined }
237+ mirror = { config ?. mirrorView ?? defaultConfig . mirrorView }
238+ onlyAssigned = { true }
239+ assignMode = { AssignMode . All }
240+ onRoleSelected = { setSelectRole }
241+ />
242+ </ div >
243+ </ div >
244+ </ div >
245+ </ >
246+ ) ;
247+ }
0 commit comments