@@ -14,17 +14,22 @@ import {
1414 SubtitleFile ,
1515 VideoFile ,
1616} from './types'
17- import { getPositionAtTime , parseFunscript , transformFunscriptActions } from './services/funscript'
17+ import { parseFunscript , transformFunscriptActions } from './services/funscript'
1818import { handyService , HandyUploadStatus } from './services/handy'
1919import {
2020 ButtplugConnectionState ,
2121 ButtplugDevice ,
22- ButtplugDeviceFrame ,
23- ButtplugFeature ,
2422 buttplugService ,
2523} from './services/buttplug'
2624import {
27- getDefaultAxisValue ,
25+ AxisActionMap ,
26+ buildButtplugDeviceSignature ,
27+ buildButtplugTransportCommand ,
28+ buildFeatureMappingsForDevice ,
29+ ButtplugFeatureMapping ,
30+ getButtplugFeatureStorageKey ,
31+ } from './services/buttplugDeviceControl'
32+ import {
2833 normalizeScriptBundle ,
2934 SCRIPT_AXIS_IDS ,
3035} from './services/multiaxis'
@@ -43,11 +48,7 @@ const BUTTPLUG_FEATURE_MAPPINGS_STORAGE_KEY = 'scriptplayer-buttplug-feature-map
4348const DEFAULT_BUTTPLUG_SERVER_URL = 'ws://127.0.0.1:12345'
4449
4550type DeviceProvider = 'handy' | 'buttplug'
46- type AxisActionMap = Partial < Record < ScriptAxisId , FunscriptAction [ ] > >
47- type StoredButtplugFeatureMapping = {
48- axisId : ScriptAxisId | ''
49- invert : boolean
50- }
51+ type StoredButtplugFeatureMapping = ButtplugFeatureMapping
5152
5253function getMediaTypeFromPath ( filePath : string ) : MediaType | null {
5354 const ext = '.' + ( filePath . split ( '.' ) . pop ( ) ?. toLowerCase ( ) || '' )
@@ -213,152 +214,6 @@ function getPrimaryAxis(bundle: FunscriptBundle | null): ScriptAxisId | null {
213214 return ( Object . keys ( bundle . scripts ) [ 0 ] as ScriptAxisId | undefined ) ?? null
214215}
215216
216- function buildButtplugDeviceSignature ( device : ButtplugDevice ) : string {
217- const featureSignature = device . features
218- . map ( ( feature ) => `${ feature . type } :${ feature . index } :${ feature . descriptor } :${ feature . actuatorType || '' } ` )
219- . join ( '|' )
220-
221- return `${ device . name } |${ device . displayName } |${ featureSignature } `
222- }
223-
224- function getButtplugFeatureStorageKey ( deviceSignature : string , featureId : string ) : string {
225- return `${ deviceSignature } ::${ featureId } `
226- }
227-
228- function guessScriptAxisForFeature ( feature : ButtplugFeature ) : ScriptAxisId | '' {
229- const text = `${ feature . descriptor } ${ feature . actuatorType || '' } ` . toLowerCase ( )
230-
231- if ( feature . type === 'linear' ) {
232- if ( text . includes ( 'stroke' ) || text . includes ( 'stroker' ) || text . includes ( 'thrust' ) ) return 'L0'
233- if ( text . includes ( 'surge' ) || text . includes ( 'forward' ) || text . includes ( 'back' ) ) return 'L1'
234- if ( text . includes ( 'sway' ) || text . includes ( 'left' ) || text . includes ( 'right' ) ) return 'L2'
235- if ( feature . index === 0 ) return 'L0'
236- if ( feature . index === 1 ) return 'L1'
237- if ( feature . index === 2 ) return 'L2'
238- }
239-
240- if ( feature . type === 'rotate' ) {
241- if ( text . includes ( 'twist' ) ) return 'R0'
242- if ( text . includes ( 'roll' ) ) return 'R1'
243- if ( text . includes ( 'pitch' ) ) return 'R2'
244- if ( feature . index === 0 ) return 'R0'
245- if ( feature . index === 1 ) return 'R1'
246- if ( feature . index === 2 ) return 'R2'
247- }
248-
249- if ( feature . type === 'scalar' ) {
250- if ( text . includes ( 'vibe' ) || text . includes ( 'vibrate' ) ) return feature . index === 0 ? 'V0' : 'V1'
251- if ( text . includes ( 'pump' ) ) return 'V1'
252- if ( text . includes ( 'valve' ) ) return 'A0'
253- if ( text . includes ( 'suck' ) || text . includes ( 'suction' ) ) return 'A1'
254- if ( text . includes ( 'lube' ) ) return 'A2'
255- }
256-
257- return ''
258- }
259-
260- function buildFeatureMappingsForDevice (
261- device : ButtplugDevice | null ,
262- mappingStore : Record < string , StoredButtplugFeatureMapping >
263- ) : Record < string , StoredButtplugFeatureMapping > {
264- if ( ! device ) return { }
265-
266- const deviceSignature = buildButtplugDeviceSignature ( device )
267- const next : Record < string , StoredButtplugFeatureMapping > = { }
268-
269- for ( const feature of device . features ) {
270- const key = getButtplugFeatureStorageKey ( deviceSignature , feature . id )
271- const stored = mappingStore [ key ]
272- next [ feature . id ] = {
273- axisId : stored ?. axisId ?? guessScriptAxisForFeature ( feature ) ,
274- invert : stored ?. invert ?? false ,
275- }
276- }
277-
278- return next
279- }
280-
281- function getAxisValueAtTime ( axisId : ScriptAxisId , actionMap : AxisActionMap , timeMs : number ) : number {
282- const actions = actionMap [ axisId ]
283- if ( ! actions || actions . length === 0 ) {
284- return getDefaultAxisValue ( axisId )
285- }
286-
287- return getPositionAtTime ( actions , timeMs ) / 100
288- }
289-
290- function applyAxisMappingValue ( axisId : ScriptAxisId , value : number , invert : boolean ) : number {
291- const safeValue = Number . isFinite ( value ) ? value : getDefaultAxisValue ( axisId )
292- return invert ? 1 - safeValue : safeValue
293- }
294-
295- function buildButtplugFrame (
296- device : ButtplugDevice ,
297- mappings : Record < string , StoredButtplugFeatureMapping > ,
298- actionMap : AxisActionMap ,
299- currentTimeMs : number ,
300- targetTimeMs : number ,
301- intervalMs : number
302- ) : ButtplugDeviceFrame {
303- const frame : ButtplugDeviceFrame = {
304- linear : [ ] ,
305- rotate : [ ] ,
306- scalar : [ ] ,
307- }
308-
309- for ( const feature of device . features ) {
310- const mapping = mappings [ feature . id ]
311- const mappedAxisId = mapping ?. axisId
312-
313- if ( ! mappedAxisId ) {
314- if ( feature . type === 'linear' ) {
315- frame . linear ?. push ( { index : feature . index , position : 0.5 , duration : intervalMs } )
316- } else if ( feature . type === 'rotate' ) {
317- frame . rotate ?. push ( { index : feature . index , speed : 0 , clockwise : true } )
318- } else if ( feature . type === 'scalar' && feature . actuatorType ) {
319- frame . scalar ?. push ( { index : feature . index , scalar : 0 , actuatorType : feature . actuatorType } )
320- }
321- continue
322- }
323-
324- const currentValue = applyAxisMappingValue (
325- mappedAxisId ,
326- getAxisValueAtTime ( mappedAxisId , actionMap , currentTimeMs ) ,
327- mapping ?. invert ?? false
328- )
329- const targetValue = applyAxisMappingValue (
330- mappedAxisId ,
331- getAxisValueAtTime ( mappedAxisId , actionMap , targetTimeMs ) ,
332- mapping ?. invert ?? false
333- )
334-
335- if ( feature . type === 'linear' ) {
336- frame . linear ?. push ( { index : feature . index , position : targetValue , duration : intervalMs } )
337- continue
338- }
339-
340- if ( feature . type === 'rotate' ) {
341- const delta = targetValue - currentValue
342- frame . rotate ?. push ( {
343- index : feature . index ,
344- speed : Math . min ( 1 , Math . abs ( delta ) * 1000 / Math . max ( intervalMs , 1 ) ) ,
345- clockwise : delta >= 0 ,
346- } )
347- continue
348- }
349-
350- if ( feature . type === 'scalar' && feature . actuatorType ) {
351- frame . scalar ?. push ( { index : feature . index , scalar : targetValue , actuatorType : feature . actuatorType } )
352- }
353- }
354-
355- return {
356- linear : frame . linear && frame . linear . length > 0 ? frame . linear : undefined ,
357- rotate : frame . rotate && frame . rotate . length > 0 ? frame . rotate : undefined ,
358- scalar : frame . scalar && frame . scalar . length > 0 ? frame . scalar : undefined ,
359- }
360- }
361-
362217export default function App ( ) {
363218 const { locale, setLocale } = useTranslation ( )
364219 const [ files , setFiles ] = useState < VideoFile [ ] > ( [ ] )
@@ -595,7 +450,7 @@ export default function App() {
595450 const effectivePlaybackRate = currentMedia . playbackRate > 0 ? currentMedia . playbackRate : playbackRate
596451 const currentTimeMs = currentMedia . currentTime * 1000 + ( settings . timeOffset || 0 )
597452 const targetTimeMs = currentTimeMs + intervalMs * effectivePlaybackRate
598- const frame = buildButtplugFrame (
453+ const command = buildButtplugTransportCommand (
599454 selectedButtplugDevice ,
600455 selectedButtplugFeatureMappings ,
601456 runtimeAxisActions ,
@@ -604,7 +459,7 @@ export default function App() {
604459 intervalMs
605460 )
606461
607- await buttplugService . sendDeviceFrame ( selectedButtplugDevice . index , frame )
462+ await buttplugService . sendDeviceFrame ( selectedButtplugDevice . index , command . frame , { rawTCode : command . rawTCode } )
608463
609464 if ( runId !== buttplugStreamRunId . current ) return
610465 buttplugStreamTimer . current = setTimeout ( ( ) => {
0 commit comments