@buildcores/render-client 1.0.2 → 1.0.4

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/dist/index.js CHANGED
@@ -4,47 +4,51 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
5
 
6
6
  // Helper to extract clientX from mouse or touch events
7
- const getClientX = (e) => {
7
+ const getClientX$1 = (e) => {
8
8
  return "touches" in e ? e.touches[0].clientX : e.clientX;
9
9
  };
10
- // Helper to calculate new video time with circular wrapping
11
- const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
12
- const timeDelta = deltaX * sensitivity;
13
- let newTime = startTime + timeDelta;
10
+ // Helper to calculate new frame with circular wrapping
11
+ const calculateCircularFrame = (startFrame, deltaX, sensitivity, totalFrames) => {
12
+ const frameDelta = deltaX * sensitivity;
13
+ let newFrame = startFrame + frameDelta;
14
14
  // Make it circular - wrap around when going past boundaries
15
- newTime = newTime % duration;
16
- if (newTime < 0) {
17
- newTime += duration;
15
+ newFrame = newFrame % totalFrames;
16
+ if (newFrame < 0) {
17
+ newFrame += totalFrames;
18
18
  }
19
- return newTime;
19
+ return newFrame;
20
20
  };
21
- const useVideoScrubbing = (videoRef, options = {}) => {
22
- const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
21
+ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
22
+ const { mouseSensitivity = 0.1, touchSensitivity = 0.1, onFrameChange, } = options;
23
23
  const [isDragging, setIsDragging] = React.useState(false);
24
24
  const [dragStartX, setDragStartX] = React.useState(0);
25
- const [dragStartTime, setDragStartTime] = React.useState(0);
25
+ const [dragStartFrame, setDragStartFrame] = React.useState(0);
26
26
  const hasDragged = React.useRef(false);
27
+ const currentFrame = React.useRef(0);
27
28
  // Helper to start dragging (common logic for mouse and touch)
28
29
  const startDrag = React.useCallback((clientX, event) => {
29
- if (!videoRef.current)
30
+ if (!canvasRef.current)
30
31
  return;
31
32
  setIsDragging(true);
32
33
  setDragStartX(clientX);
33
- setDragStartTime(videoRef.current.currentTime);
34
+ setDragStartFrame(currentFrame.current);
34
35
  hasDragged.current = true;
35
36
  event.preventDefault();
36
- }, [videoRef]);
37
+ }, [canvasRef]);
37
38
  // Helper to handle drag movement (common logic for mouse and touch)
38
39
  const handleDragMove = React.useCallback((clientX, sensitivity) => {
39
- if (!isDragging || !videoRef.current)
40
+ if (!isDragging || !canvasRef.current)
40
41
  return;
41
42
  const deltaX = clientX - dragStartX;
42
- const duration = videoRef.current.duration || 0;
43
- if (duration > 0) {
44
- const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
45
- videoRef.current.currentTime = newTime;
43
+ const newFrame = calculateCircularFrame(dragStartFrame, deltaX, // Positive for natural "spin right" feel
44
+ sensitivity, totalFrames);
45
+ currentFrame.current = newFrame;
46
+ // Call the frame change callback if provided
47
+ if (onFrameChange) {
48
+ onFrameChange(newFrame);
46
49
  }
47
- }, [isDragging, dragStartX, dragStartTime, videoRef]);
50
+ return newFrame;
51
+ }, [isDragging, dragStartX, dragStartFrame, totalFrames, onFrameChange]);
48
52
  // Helper to end dragging (common logic for mouse and touch)
49
53
  const endDrag = React.useCallback(() => {
50
54
  setIsDragging(false);
@@ -56,10 +60,10 @@ const useVideoScrubbing = (videoRef, options = {}) => {
56
60
  startDrag(e.touches[0].clientX, e.nativeEvent);
57
61
  }, [startDrag]);
58
62
  const handleDocumentMouseMove = React.useCallback((e) => {
59
- handleDragMove(getClientX(e), mouseSensitivity);
63
+ return handleDragMove(getClientX$1(e), mouseSensitivity);
60
64
  }, [handleDragMove, mouseSensitivity]);
61
65
  const handleDocumentTouchMove = React.useCallback((e) => {
62
- handleDragMove(getClientX(e), touchSensitivity);
66
+ return handleDragMove(getClientX$1(e), touchSensitivity);
63
67
  }, [handleDragMove, touchSensitivity]);
64
68
  const handleDocumentMouseUp = React.useCallback(() => {
65
69
  endDrag();
@@ -93,6 +97,10 @@ const useVideoScrubbing = (videoRef, options = {}) => {
93
97
  handleMouseDown,
94
98
  handleTouchStart,
95
99
  hasDragged,
100
+ currentFrame: currentFrame.current,
101
+ setCurrentFrame: (frame) => {
102
+ currentFrame.current = frame;
103
+ },
96
104
  };
97
105
  };
98
106
 
@@ -313,48 +321,6 @@ function useBouncePatternProgress(enabled = true) {
313
321
  return { value, isBouncing };
314
322
  }
315
323
 
316
- // API Types
317
- /**
318
- * Enum defining all available PC part categories that can be rendered.
319
- *
320
- * Each category represents a different type of computer component that can be
321
- * included in the 3D build visualization.
322
- *
323
- * @example
324
- * ```tsx
325
- * // All available categories
326
- * const categories = [
327
- * PartCategory.CPU, // "CPU"
328
- * PartCategory.GPU, // "GPU"
329
- * PartCategory.RAM, // "RAM"
330
- * PartCategory.Motherboard,// "Motherboard"
331
- * PartCategory.PSU, // "PSU"
332
- * PartCategory.Storage, // "Storage"
333
- * PartCategory.PCCase, // "PCCase"
334
- * PartCategory.CPUCooler, // "CPUCooler"
335
- * ];
336
- * ```
337
- */
338
- exports.PartCategory = void 0;
339
- (function (PartCategory) {
340
- /** Central Processing Unit - The main processor */
341
- PartCategory["CPU"] = "CPU";
342
- /** Graphics Processing Unit - Video card for rendering */
343
- PartCategory["GPU"] = "GPU";
344
- /** Random Access Memory - System memory modules */
345
- PartCategory["RAM"] = "RAM";
346
- /** Main circuit board that connects all components */
347
- PartCategory["Motherboard"] = "Motherboard";
348
- /** Power Supply Unit - Provides power to all components */
349
- PartCategory["PSU"] = "PSU";
350
- /** Storage devices like SSDs, HDDs, NVMe drives */
351
- PartCategory["Storage"] = "Storage";
352
- /** PC Case - The enclosure that houses all components */
353
- PartCategory["PCCase"] = "PCCase";
354
- /** CPU Cooler - Air or liquid cooling for the processor */
355
- PartCategory["CPUCooler"] = "CPUCooler";
356
- })(exports.PartCategory || (exports.PartCategory = {}));
357
-
358
324
  // API Configuration
359
325
  const API_BASE_URL = "https://squid-app-7aeyk.ondigitalocean.app";
360
326
  // API Endpoints
@@ -368,12 +334,16 @@ const buildApiUrl = (endpoint) => {
368
334
  };
369
335
  // API Implementation
370
336
  const renderBuildExperimental = async (request) => {
337
+ const requestWithFormat = {
338
+ ...request,
339
+ format: request.format || "video", // Default to video format
340
+ };
371
341
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
372
342
  method: "POST",
373
343
  headers: {
374
344
  "Content-Type": "application/json",
375
345
  },
376
- body: JSON.stringify(request),
346
+ body: JSON.stringify(requestWithFormat),
377
347
  });
378
348
  if (!response.ok) {
379
349
  throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
@@ -387,6 +357,33 @@ const renderBuildExperimental = async (request) => {
387
357
  },
388
358
  };
389
359
  };
360
+ const renderSpriteExperimental = async (request) => {
361
+ const requestWithFormat = {
362
+ ...request,
363
+ format: "sprite",
364
+ };
365
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
366
+ method: "POST",
367
+ headers: {
368
+ "Content-Type": "application/json",
369
+ },
370
+ body: JSON.stringify(requestWithFormat),
371
+ });
372
+ if (!response.ok) {
373
+ throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
374
+ }
375
+ const sprite = await response.blob();
376
+ return {
377
+ sprite,
378
+ metadata: {
379
+ cols: 12, // Default sprite grid - could be returned from API
380
+ rows: 6,
381
+ totalFrames: 72,
382
+ size: sprite.size,
383
+ format: "image/webp",
384
+ },
385
+ };
386
+ };
390
387
  const getAvailableParts = async () => {
391
388
  const response = await fetch(buildApiUrl(API_ENDPOINTS.AVAILABLE_PARTS), {
392
389
  method: "GET",
@@ -400,6 +397,48 @@ const getAvailableParts = async () => {
400
397
  return response.json();
401
398
  };
402
399
 
400
+ // API Types
401
+ /**
402
+ * Enum defining all available PC part categories that can be rendered.
403
+ *
404
+ * Each category represents a different type of computer component that can be
405
+ * included in the 3D build visualization.
406
+ *
407
+ * @example
408
+ * ```tsx
409
+ * // All available categories
410
+ * const categories = [
411
+ * PartCategory.CPU, // "CPU"
412
+ * PartCategory.GPU, // "GPU"
413
+ * PartCategory.RAM, // "RAM"
414
+ * PartCategory.Motherboard,// "Motherboard"
415
+ * PartCategory.PSU, // "PSU"
416
+ * PartCategory.Storage, // "Storage"
417
+ * PartCategory.PCCase, // "PCCase"
418
+ * PartCategory.CPUCooler, // "CPUCooler"
419
+ * ];
420
+ * ```
421
+ */
422
+ exports.PartCategory = void 0;
423
+ (function (PartCategory) {
424
+ /** Central Processing Unit - The main processor */
425
+ PartCategory["CPU"] = "CPU";
426
+ /** Graphics Processing Unit - Video card for rendering */
427
+ PartCategory["GPU"] = "GPU";
428
+ /** Random Access Memory - System memory modules */
429
+ PartCategory["RAM"] = "RAM";
430
+ /** Main circuit board that connects all components */
431
+ PartCategory["Motherboard"] = "Motherboard";
432
+ /** Power Supply Unit - Provides power to all components */
433
+ PartCategory["PSU"] = "PSU";
434
+ /** Storage devices like SSDs, HDDs, NVMe drives */
435
+ PartCategory["Storage"] = "Storage";
436
+ /** PC Case - The enclosure that houses all components */
437
+ PartCategory["PCCase"] = "PCCase";
438
+ /** CPU Cooler - Air or liquid cooling for the processor */
439
+ PartCategory["CPUCooler"] = "CPUCooler";
440
+ })(exports.PartCategory || (exports.PartCategory = {}));
441
+
403
442
  /**
404
443
  * Compares two RenderBuildRequest objects for equality by checking if the same IDs
405
444
  * are present in each category array, regardless of order.
@@ -478,6 +517,65 @@ const useBuildRender = (parts, onLoadStart) => {
478
517
  };
479
518
  };
480
519
 
520
+ const useSpriteRender = (parts, onLoadStart) => {
521
+ const [spriteSrc, setSpriteSrc] = React.useState(null);
522
+ const [isRenderingSprite, setIsRenderingSprite] = React.useState(false);
523
+ const [renderError, setRenderError] = React.useState(null);
524
+ const [spriteMetadata, setSpriteMetadata] = React.useState(null);
525
+ const previousPartsRef = React.useRef(null);
526
+ const fetchRenderSprite = React.useCallback(async (currentParts) => {
527
+ try {
528
+ setIsRenderingSprite(true);
529
+ setRenderError(null);
530
+ onLoadStart?.();
531
+ const response = await renderSpriteExperimental(currentParts);
532
+ const objectUrl = URL.createObjectURL(response.sprite);
533
+ // Clean up previous sprite URL before setting new one
534
+ setSpriteSrc((prevSrc) => {
535
+ if (prevSrc) {
536
+ URL.revokeObjectURL(prevSrc);
537
+ }
538
+ return objectUrl;
539
+ });
540
+ // Set sprite metadata
541
+ setSpriteMetadata({
542
+ cols: response.metadata?.cols || 12,
543
+ rows: response.metadata?.rows || 6,
544
+ totalFrames: response.metadata?.totalFrames || 72,
545
+ });
546
+ }
547
+ catch (error) {
548
+ setRenderError(error instanceof Error ? error.message : "Failed to render sprite");
549
+ }
550
+ finally {
551
+ setIsRenderingSprite(false);
552
+ }
553
+ }, [onLoadStart]);
554
+ // Effect to call API when parts content changes (using custom equality check)
555
+ React.useEffect(() => {
556
+ const shouldFetch = previousPartsRef.current === null ||
557
+ !arePartsEqual(previousPartsRef.current, parts);
558
+ if (shouldFetch) {
559
+ previousPartsRef.current = parts;
560
+ fetchRenderSprite(parts);
561
+ }
562
+ }, [parts, fetchRenderSprite]);
563
+ // Cleanup effect for component unmount
564
+ React.useEffect(() => {
565
+ return () => {
566
+ if (spriteSrc) {
567
+ URL.revokeObjectURL(spriteSrc);
568
+ }
569
+ };
570
+ }, [spriteSrc]);
571
+ return {
572
+ spriteSrc,
573
+ isRenderingSprite,
574
+ renderError,
575
+ spriteMetadata,
576
+ };
577
+ };
578
+
481
579
  const LoadingErrorOverlay = ({ isVisible, renderError, size, }) => {
482
580
  if (!isVisible)
483
581
  return null;
@@ -513,7 +611,7 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
513
611
  position: "absolute",
514
612
  top: "50%",
515
613
  left: "50%",
516
- transform: `translate(-50%, -50%) translateX(${progressValue * 100}px)`,
614
+ transform: `translate(-50%, -50%) translateX(${progressValue * 50}px)`,
517
615
  backgroundColor: "rgba(0, 0, 0, 0.8)",
518
616
  color: "white",
519
617
  padding: "12px",
@@ -532,54 +630,214 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
532
630
  } })) }));
533
631
  };
534
632
 
535
- const BuildRender = ({ parts, size, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
536
- const videoRef = React.useRef(null);
633
+ const BuildRender = ({ parts, size, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
634
+ const canvasRef = React.useRef(null);
635
+ const [img, setImg] = React.useState(null);
537
636
  const [isLoading, setIsLoading] = React.useState(true);
538
637
  const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
539
- // Use custom hook for build rendering
540
- const { videoSrc, isRenderingBuild, renderError } = useBuildRender(parts);
638
+ // Use custom hook for sprite rendering
639
+ const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(parts);
541
640
  const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
542
- const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useVideoScrubbing(videoRef, {
641
+ const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
642
+ const cols = spriteMetadata ? spriteMetadata.cols : 12;
643
+ const rows = spriteMetadata ? spriteMetadata.rows : 6;
644
+ const frameRef = React.useRef(0);
645
+ // Image/frame sizes
646
+ const frameW = img ? img.width / cols : 0;
647
+ const frameH = img ? img.height / rows : 0;
648
+ // ---- Load sprite image ----
649
+ React.useEffect(() => {
650
+ if (!spriteSrc) {
651
+ setImg(null);
652
+ setIsLoading(true);
653
+ return;
654
+ }
655
+ setIsLoading(true);
656
+ const i = new Image();
657
+ i.decoding = "async";
658
+ i.loading = "eager";
659
+ i.src = spriteSrc;
660
+ i.onload = () => {
661
+ setImg(i);
662
+ setIsLoading(false);
663
+ // Start bouncing animation after delay
664
+ setTimeout(() => {
665
+ setBouncingAllowed(true);
666
+ }, 2000);
667
+ };
668
+ i.onerror = () => {
669
+ setImg(null);
670
+ setIsLoading(false);
671
+ };
672
+ }, [spriteSrc]);
673
+ // ---- Drawing function ----
674
+ const draw = React.useCallback((frameIndex) => {
675
+ const cnv = canvasRef.current;
676
+ if (!cnv || !img || !frameW || !frameH)
677
+ return;
678
+ const ctx = cnv.getContext("2d");
679
+ if (!ctx)
680
+ return;
681
+ // Backing store sized for HiDPI; CSS size stays `size`
682
+ const dpr = Math.max(1, window.devicePixelRatio || 1);
683
+ const targetW = Math.round(size * dpr);
684
+ const targetH = Math.round(size * dpr);
685
+ if (cnv.width !== targetW || cnv.height !== targetH) {
686
+ cnv.width = targetW;
687
+ cnv.height = targetH;
688
+ }
689
+ // Snap to integer frame (never between tiles)
690
+ let n = Math.round(frameIndex) % total;
691
+ if (n < 0)
692
+ n += total;
693
+ const r = Math.floor(n / cols);
694
+ const c = n % cols;
695
+ // Use integer source rects to avoid sampling bleed across tiles
696
+ const sx = Math.round(c * frameW);
697
+ const sy = Math.round(r * frameH);
698
+ const sw = Math.round(frameW);
699
+ const sh = Math.round(frameH);
700
+ ctx.clearRect(0, 0, targetW, targetH);
701
+ ctx.imageSmoothingEnabled = true;
702
+ ctx.imageSmoothingQuality = "high";
703
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
704
+ }, [img, frameW, frameH, size, cols, total]);
705
+ const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
543
706
  mouseSensitivity,
544
707
  touchSensitivity,
708
+ onFrameChange: (newFrame) => {
709
+ frameRef.current = newFrame;
710
+ draw(newFrame);
711
+ },
545
712
  });
546
- const handleLoadStartInternal = React.useCallback(() => {
713
+ React.useCallback(() => {
547
714
  setIsLoading(true);
548
715
  setBouncingAllowed(false);
549
716
  }, []);
550
- const handleCanPlayInternal = React.useCallback(() => {
551
- setIsLoading(false);
552
- // Start bouncing animation after delay
553
- setTimeout(() => {
554
- setBouncingAllowed(true);
555
- }, 2000);
556
- }, []);
717
+ // Auto-rotate when bouncing is allowed and not dragged
557
718
  React.useEffect(() => {
558
- if (hasDragged.current || !videoRef.current)
719
+ if (hasDragged.current || !img)
559
720
  return;
560
- const duration = videoRef.current.duration;
561
- if (!isFinite(duration))
562
- return;
563
- const time = calculateCircularTime(0, progressValue, 0.5, duration);
564
- if (isFinite(time)) {
565
- videoRef.current.currentTime = time;
721
+ // Calculate frame based on progress value (similar to video time calculation)
722
+ const frame = ((progressValue / 5) * total) % total;
723
+ frameRef.current = frame;
724
+ draw(frame);
725
+ }, [progressValue, hasDragged, img, total, draw]);
726
+ // Initial draw once image is ready
727
+ React.useEffect(() => {
728
+ if (img && !isLoading) {
729
+ draw(frameRef.current);
566
730
  }
567
- }, [progressValue, hasDragged]);
568
- return (jsxRuntime.jsxs("div", { style: { position: "relative", width: size, height: size }, children: [videoSrc && (jsxRuntime.jsx("video", { ref: videoRef, src: videoSrc, width: size, height: size, autoPlay: true, preload: "metadata", muted: true, playsInline: true, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onLoadStart: handleLoadStartInternal, onCanPlay: handleCanPlayInternal, onLoadedData: () => {
569
- if (videoRef.current) {
570
- videoRef.current.pause();
571
- }
572
- }, style: {
731
+ }, [img, isLoading, draw]);
732
+ return (jsxRuntime.jsxs("div", { style: { position: "relative", width: size, height: size }, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, style: {
733
+ width: size,
734
+ height: size,
573
735
  cursor: isDragging ? "grabbing" : "grab",
574
736
  touchAction: "none", // Prevents default touch behaviors like scrolling
575
737
  display: "block",
576
- }, children: "Your browser does not support the video tag." }, videoSrc)), jsxRuntime.jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingBuild || !!renderError, renderError: renderError || undefined, size: size }), jsxRuntime.jsx(InstructionTooltip, { isVisible: !isLoading &&
577
- !isRenderingBuild &&
738
+ userSelect: "none",
739
+ WebkitUserSelect: "none",
740
+ WebkitTouchCallout: "none",
741
+ }, role: "img", "aria-label": "360\u00B0 viewer", onContextMenu: (e) => e.preventDefault() })), jsxRuntime.jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingSprite || !!renderError, renderError: renderError || undefined, size: size }), jsxRuntime.jsx(InstructionTooltip, { isVisible: !isLoading &&
742
+ !isRenderingSprite &&
578
743
  !renderError &&
579
744
  isBouncing &&
580
745
  !hasDragged.current, progressValue: progressValue })] }));
581
746
  };
582
747
 
748
+ // Helper to extract clientX from mouse or touch events
749
+ const getClientX = (e) => {
750
+ return "touches" in e ? e.touches[0].clientX : e.clientX;
751
+ };
752
+ // Helper to calculate new video time with circular wrapping
753
+ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
754
+ const timeDelta = deltaX * sensitivity;
755
+ let newTime = startTime + timeDelta;
756
+ // Make it circular - wrap around when going past boundaries
757
+ newTime = newTime % duration;
758
+ if (newTime < 0) {
759
+ newTime += duration;
760
+ }
761
+ return newTime;
762
+ };
763
+ const useVideoScrubbing = (videoRef, options = {}) => {
764
+ const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
765
+ const [isDragging, setIsDragging] = React.useState(false);
766
+ const [dragStartX, setDragStartX] = React.useState(0);
767
+ const [dragStartTime, setDragStartTime] = React.useState(0);
768
+ const hasDragged = React.useRef(false);
769
+ // Helper to start dragging (common logic for mouse and touch)
770
+ const startDrag = React.useCallback((clientX, event) => {
771
+ if (!videoRef.current)
772
+ return;
773
+ setIsDragging(true);
774
+ setDragStartX(clientX);
775
+ setDragStartTime(videoRef.current.currentTime);
776
+ hasDragged.current = true;
777
+ event.preventDefault();
778
+ }, [videoRef]);
779
+ // Helper to handle drag movement (common logic for mouse and touch)
780
+ const handleDragMove = React.useCallback((clientX, sensitivity) => {
781
+ if (!isDragging || !videoRef.current)
782
+ return;
783
+ const deltaX = clientX - dragStartX;
784
+ const duration = videoRef.current.duration || 0;
785
+ if (duration > 0) {
786
+ const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
787
+ videoRef.current.currentTime = newTime;
788
+ }
789
+ }, [isDragging, dragStartX, dragStartTime, videoRef]);
790
+ // Helper to end dragging (common logic for mouse and touch)
791
+ const endDrag = React.useCallback(() => {
792
+ setIsDragging(false);
793
+ }, []);
794
+ const handleMouseDown = React.useCallback((e) => {
795
+ startDrag(e.clientX, e.nativeEvent);
796
+ }, [startDrag]);
797
+ const handleTouchStart = React.useCallback((e) => {
798
+ startDrag(e.touches[0].clientX, e.nativeEvent);
799
+ }, [startDrag]);
800
+ const handleDocumentMouseMove = React.useCallback((e) => {
801
+ handleDragMove(getClientX(e), mouseSensitivity);
802
+ }, [handleDragMove, mouseSensitivity]);
803
+ const handleDocumentTouchMove = React.useCallback((e) => {
804
+ handleDragMove(getClientX(e), touchSensitivity);
805
+ }, [handleDragMove, touchSensitivity]);
806
+ const handleDocumentMouseUp = React.useCallback(() => {
807
+ endDrag();
808
+ }, [endDrag]);
809
+ const handleDocumentTouchEnd = React.useCallback(() => {
810
+ endDrag();
811
+ }, [endDrag]);
812
+ // Add document-level event listeners when dragging starts
813
+ React.useEffect(() => {
814
+ if (isDragging) {
815
+ document.addEventListener("mousemove", handleDocumentMouseMove);
816
+ document.addEventListener("mouseup", handleDocumentMouseUp);
817
+ document.addEventListener("touchmove", handleDocumentTouchMove);
818
+ document.addEventListener("touchend", handleDocumentTouchEnd);
819
+ return () => {
820
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
821
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
822
+ document.removeEventListener("touchmove", handleDocumentTouchMove);
823
+ document.removeEventListener("touchend", handleDocumentTouchEnd);
824
+ };
825
+ }
826
+ }, [
827
+ isDragging,
828
+ handleDocumentMouseMove,
829
+ handleDocumentMouseUp,
830
+ handleDocumentTouchMove,
831
+ handleDocumentTouchEnd,
832
+ ]);
833
+ return {
834
+ isDragging,
835
+ handleMouseDown,
836
+ handleTouchStart,
837
+ hasDragged,
838
+ };
839
+ };
840
+
583
841
  exports.API_BASE_URL = API_BASE_URL;
584
842
  exports.API_ENDPOINTS = API_ENDPOINTS;
585
843
  exports.BuildRender = BuildRender;
@@ -588,10 +846,14 @@ exports.InstructionTooltip = InstructionTooltip;
588
846
  exports.LoadingErrorOverlay = LoadingErrorOverlay;
589
847
  exports.arePartsEqual = arePartsEqual;
590
848
  exports.buildApiUrl = buildApiUrl;
849
+ exports.calculateCircularFrame = calculateCircularFrame;
591
850
  exports.calculateCircularTime = calculateCircularTime;
592
851
  exports.getAvailableParts = getAvailableParts;
593
852
  exports.renderBuildExperimental = renderBuildExperimental;
853
+ exports.renderSpriteExperimental = renderSpriteExperimental;
594
854
  exports.useBouncePatternProgress = useBouncePatternProgress;
595
855
  exports.useBuildRender = useBuildRender;
856
+ exports.useSpriteRender = useSpriteRender;
857
+ exports.useSpriteScrubbing = useSpriteScrubbing;
596
858
  exports.useVideoScrubbing = useVideoScrubbing;
597
859
  //# sourceMappingURL=index.js.map