@limrun/ui 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -319,7 +319,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
319
319
 
320
320
  // Minimal geometry for single-finger touch events (no mirror/container coords needed).
321
321
  type PointerGeometry = {
322
- inside: boolean;
323
322
  videoX: number;
324
323
  videoY: number;
325
324
  videoWidth: number;
@@ -332,7 +331,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
332
331
  geometry: PointerGeometry | null,
333
332
  ) => {
334
333
  if (!geometry) return;
335
- const { inside: isInside, videoX, videoY, videoWidth, videoHeight } = geometry;
334
+ const { videoX, videoY, videoWidth, videoHeight } = geometry;
336
335
 
337
336
  let action: number | null = null;
338
337
  let positionToSend: { x: number; y: number } | null = null;
@@ -341,35 +340,26 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
341
340
 
342
341
  switch (eventType) {
343
342
  case 'down':
344
- if (isInside) {
345
- // For multi-touch: use ACTION_DOWN for first pointer, ACTION_POINTER_DOWN for additional pointers
346
- const currentPointerCount = activePointers.current.size;
347
- action =
348
- currentPointerCount === 0
349
- ? AMOTION_EVENT.ACTION_DOWN
350
- : AMOTION_EVENT.ACTION_POINTER_DOWN;
351
- positionToSend = { x: videoX, y: videoY };
352
- activePointers.current.set(pointerId, positionToSend);
353
- if (pointerId === -1) {
354
- // Focus on mouse down
355
- videoRef.current?.focus();
356
- }
357
- } else {
358
- // If the initial down event is outside, ignore it for this pointer
359
- activePointers.current.delete(pointerId);
343
+ // For multi-touch: use ACTION_DOWN for first pointer, ACTION_POINTER_DOWN for additional pointers
344
+ const currentPointerCount = activePointers.current.size;
345
+ action =
346
+ currentPointerCount === 0
347
+ ? AMOTION_EVENT.ACTION_DOWN
348
+ : AMOTION_EVENT.ACTION_POINTER_DOWN;
349
+ positionToSend = { x: videoX, y: videoY };
350
+ activePointers.current.set(pointerId, positionToSend);
351
+ if (pointerId === -1) {
352
+ // Focus on mouse down
353
+ videoRef.current?.focus();
360
354
  }
361
355
  break;
362
356
 
363
357
  case 'move':
364
358
  if (activePointers.current.has(pointerId)) {
365
- if (isInside) {
366
- action = AMOTION_EVENT.ACTION_MOVE;
367
- positionToSend = { x: videoX, y: videoY };
368
- // Update the last known position for this active pointer
369
- activePointers.current.set(pointerId, positionToSend);
370
- } else {
371
- // Moved outside while active - do nothing, UP/CANCEL will use last known pos
372
- }
359
+ action = AMOTION_EVENT.ACTION_MOVE;
360
+ positionToSend = { x: videoX, y: videoY };
361
+ // Update the last known position for this active pointer
362
+ activePointers.current.set(pointerId, positionToSend);
373
363
  }
374
364
  break;
375
365
 
@@ -401,7 +391,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
401
391
  eventType,
402
392
  action,
403
393
  actionName: motionActionToString(action),
404
- isInside,
405
394
  positionToSend,
406
395
  video: { width: videoWidth, height: videoHeight },
407
396
  altHeld: isAltHeldRef.current,
@@ -431,7 +420,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
431
420
  sendBinaryControlMessage(message);
432
421
  }
433
422
  } else if (eventType === 'up' || eventType === 'cancel') {
434
- // Clean up map just in case if 'down' was outside and 'up'/'cancel' is triggered
435
423
  activePointers.current.delete(pointerId);
436
424
  }
437
425
  };
@@ -524,8 +512,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
524
512
  };
525
513
  };
526
514
 
527
- // Map a client point to video coordinates using a pre-computed context.
528
- // Returns null if outside the video content area or context is missing.
515
+ // Map a client point to video coordinates using a pre-computed context,
516
+ // clamping points outside the rendered video to the nearest point on the video.
529
517
  const mapClientPointToVideo = (
530
518
  ctx: VideoMappingContext,
531
519
  clientX: number,
@@ -534,25 +522,12 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
534
522
  const relativeX = clientX - ctx.videoRect.left - ctx.offsetX;
535
523
  const relativeY = clientY - ctx.videoRect.top - ctx.offsetY;
536
524
 
537
- const isInside =
538
- relativeX >= 0 && relativeX <= ctx.actualWidth &&
539
- relativeY >= 0 && relativeY <= ctx.actualHeight;
540
-
541
- if (!isInside) {
542
- return {
543
- inside: false,
544
- videoX: 0,
545
- videoY: 0,
546
- videoWidth: ctx.videoWidth,
547
- videoHeight: ctx.videoHeight,
548
- };
549
- }
550
-
551
- const videoX = Math.max(0, Math.min(ctx.videoWidth, (relativeX / ctx.actualWidth) * ctx.videoWidth));
552
- const videoY = Math.max(0, Math.min(ctx.videoHeight, (relativeY / ctx.actualHeight) * ctx.videoHeight));
525
+ const clampedRelativeX = Math.max(0, Math.min(ctx.actualWidth, relativeX));
526
+ const clampedRelativeY = Math.max(0, Math.min(ctx.actualHeight, relativeY));
527
+ const videoX = Math.max(0, Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth));
528
+ const videoY = Math.max(0, Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight));
553
529
 
554
530
  return {
555
- inside: true,
556
531
  videoX,
557
532
  videoY,
558
533
  videoWidth: ctx.videoWidth,
@@ -560,7 +535,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
560
535
  };
561
536
  };
562
537
 
563
- // Compute full hover point with mirror/container coordinates (for Alt indicator rendering).
538
+ // Compute full hover point with mirror/container coordinates (for Alt indicator rendering),
539
+ // clamping points outside the rendered video to the nearest point on the video.
564
540
  const computeFullHoverPoint = (
565
541
  ctx: VideoMappingContext,
566
542
  clientX: number,
@@ -569,25 +545,19 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
569
545
  const relativeX = clientX - ctx.videoRect.left - ctx.offsetX;
570
546
  const relativeY = clientY - ctx.videoRect.top - ctx.offsetY;
571
547
 
572
- const isInside =
573
- relativeX >= 0 && relativeX <= ctx.actualWidth &&
574
- relativeY >= 0 && relativeY <= ctx.actualHeight;
575
-
576
- if (!isInside) {
577
- return null;
578
- }
579
-
580
- const videoX = Math.max(0, Math.min(ctx.videoWidth, (relativeX / ctx.actualWidth) * ctx.videoWidth));
581
- const videoY = Math.max(0, Math.min(ctx.videoHeight, (relativeY / ctx.actualHeight) * ctx.videoHeight));
548
+ const clampedRelativeX = Math.max(0, Math.min(ctx.actualWidth, relativeX));
549
+ const clampedRelativeY = Math.max(0, Math.min(ctx.actualHeight, relativeY));
550
+ const videoX = Math.max(0, Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth));
551
+ const videoY = Math.max(0, Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight));
582
552
  const mirrorVideoX = ctx.videoWidth - videoX;
583
553
  const mirrorVideoY = ctx.videoHeight - videoY;
584
554
 
585
555
  const contentLeft = ctx.videoRect.left + ctx.offsetX;
586
556
  const contentTop = ctx.videoRect.top + ctx.offsetY;
587
- const containerX = contentLeft - ctx.containerRect.left + relativeX;
588
- const containerY = contentTop - ctx.containerRect.top + relativeY;
589
- const mirrorContainerX = contentLeft - ctx.containerRect.left + (ctx.actualWidth - relativeX);
590
- const mirrorContainerY = contentTop - ctx.containerRect.top + (ctx.actualHeight - relativeY);
557
+ const containerX = contentLeft - ctx.containerRect.left + clampedRelativeX;
558
+ const containerY = contentTop - ctx.containerRect.top + clampedRelativeY;
559
+ const mirrorContainerX = contentLeft - ctx.containerRect.left + (ctx.actualWidth - clampedRelativeX);
560
+ const mirrorContainerY = contentTop - ctx.containerRect.top + (ctx.actualHeight - clampedRelativeY);
591
561
 
592
562
  return {
593
563
  containerX,
@@ -751,29 +721,25 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
751
721
 
752
722
  if (!twoFingerStateRef.current) {
753
723
  // Starting a new two-finger gesture
754
- if (g0.inside && g1.inside) {
755
- twoFingerStateRef.current = {
756
- finger0: { x: g0.videoX, y: g0.videoY },
757
- finger1: { x: g1.videoX, y: g1.videoY },
758
- videoSize: { width: g0.videoWidth, height: g0.videoHeight },
759
- source: 'real-touch',
760
- pointerId0: t0.identifier,
761
- pointerId1: t1.identifier,
762
- };
763
- applyTwoFingerEvent('down', g0.videoWidth, g0.videoHeight,
764
- g0.videoX, g0.videoY, g1.videoX, g1.videoY,
765
- t0.identifier, t1.identifier);
766
- }
724
+ twoFingerStateRef.current = {
725
+ finger0: { x: g0.videoX, y: g0.videoY },
726
+ finger1: { x: g1.videoX, y: g1.videoY },
727
+ videoSize: { width: g0.videoWidth, height: g0.videoHeight },
728
+ source: 'real-touch',
729
+ pointerId0: t0.identifier,
730
+ pointerId1: t1.identifier,
731
+ };
732
+ applyTwoFingerEvent('down', g0.videoWidth, g0.videoHeight,
733
+ g0.videoX, g0.videoY, g1.videoX, g1.videoY,
734
+ t0.identifier, t1.identifier);
767
735
  } else if (twoFingerStateRef.current.source === 'real-touch') {
768
736
  // Continuing two-finger gesture (move)
769
- if (g0.inside && g1.inside) {
770
- twoFingerStateRef.current.finger0 = { x: g0.videoX, y: g0.videoY };
771
- twoFingerStateRef.current.finger1 = { x: g1.videoX, y: g1.videoY };
772
- applyTwoFingerEvent('move', g0.videoWidth, g0.videoHeight,
773
- g0.videoX, g0.videoY, g1.videoX, g1.videoY,
774
- twoFingerStateRef.current.pointerId0,
775
- twoFingerStateRef.current.pointerId1);
776
- }
737
+ twoFingerStateRef.current.finger0 = { x: g0.videoX, y: g0.videoY };
738
+ twoFingerStateRef.current.finger1 = { x: g1.videoX, y: g1.videoY };
739
+ applyTwoFingerEvent('move', g0.videoWidth, g0.videoHeight,
740
+ g0.videoX, g0.videoY, g1.videoX, g1.videoY,
741
+ twoFingerStateRef.current.pointerId0,
742
+ twoFingerStateRef.current.pointerId1);
777
743
  }
778
744
  } else if (allTouches.length < 2 && twoFingerStateRef.current?.source === 'real-touch') {
779
745
  // Finger lifted - end two-finger gesture using last known state
@@ -841,7 +807,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
841
807
  altHeldRef: isAltHeldRef.current,
842
808
  inTwoFingerMode,
843
809
  geometry: {
844
- inside: geometry.inside,
845
810
  videoX: geometry.videoX,
846
811
  videoY: geometry.videoY,
847
812
  videoWidth: geometry.videoWidth,
@@ -868,44 +833,41 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
868
833
  eventType: 'down' | 'move' | 'up' | 'cancel',
869
834
  geometry: PointerGeometry,
870
835
  ) => {
871
- const { inside, videoX, videoY, videoWidth, videoHeight } = geometry;
836
+ const { videoX, videoY, videoWidth, videoHeight } = geometry;
872
837
  const mirrorX = videoWidth - videoX;
873
838
  const mirrorY = videoHeight - videoY;
874
839
 
875
840
  if (eventType === 'down') {
876
- if (inside) {
877
- // Start two-finger gesture
878
- twoFingerStateRef.current = {
879
- finger0: { x: videoX, y: videoY },
880
- finger1: { x: mirrorX, y: mirrorY },
881
- videoSize: { width: videoWidth, height: videoHeight },
882
- source: 'alt-mouse',
883
- pointerId0: ALT_POINTER_ID_PRIMARY,
884
- pointerId1: ALT_POINTER_ID_MIRROR,
885
- };
886
- videoRef.current?.focus();
887
- applyTwoFingerEvent('down', videoWidth, videoHeight, videoX, videoY, mirrorX, mirrorY,
888
- ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
889
- }
841
+ // Start two-finger gesture
842
+ twoFingerStateRef.current = {
843
+ finger0: { x: videoX, y: videoY },
844
+ finger1: { x: mirrorX, y: mirrorY },
845
+ videoSize: { width: videoWidth, height: videoHeight },
846
+ source: 'alt-mouse',
847
+ pointerId0: ALT_POINTER_ID_PRIMARY,
848
+ pointerId1: ALT_POINTER_ID_MIRROR,
849
+ };
850
+ videoRef.current?.focus();
851
+ applyTwoFingerEvent('down', videoWidth, videoHeight, videoX, videoY, mirrorX, mirrorY,
852
+ ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
890
853
  return;
891
854
  }
892
855
 
893
856
  if (eventType === 'move') {
894
- if (twoFingerStateRef.current?.source === 'alt-mouse' && inside) {
857
+ if (twoFingerStateRef.current?.source === 'alt-mouse') {
895
858
  // Update positions
896
859
  twoFingerStateRef.current.finger0 = { x: videoX, y: videoY };
897
860
  twoFingerStateRef.current.finger1 = { x: mirrorX, y: mirrorY };
898
861
  applyTwoFingerEvent('move', videoWidth, videoHeight, videoX, videoY, mirrorX, mirrorY,
899
862
  ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
900
863
  }
901
- // If outside, we just don't send a move - UP will use last known position
902
864
  return;
903
865
  }
904
866
 
905
867
  if (eventType === 'up' || eventType === 'cancel') {
906
868
  const state = twoFingerStateRef.current;
907
869
  if (state?.source === 'alt-mouse') {
908
- // End gesture at last known inside positions
870
+ // End gesture at last known positions
909
871
  const { finger0, finger1, videoSize } = state;
910
872
  applyTwoFingerEvent('up', videoSize.width, videoSize.height,
911
873
  finger0.x, finger0.y, finger1.x, finger1.y,