@buildcores/render-client 1.2.0 → 1.3.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/dist/hooks/useZoomPan.d.ts +16 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.esm.js +160 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +160 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type TouchEvent as ReactTouchEvent, type WheelEvent as ReactWheelEvent } from "react";
|
|
2
|
+
interface UseZoomOptions {
|
|
3
|
+
displayWidth?: number;
|
|
4
|
+
displayHeight?: number;
|
|
5
|
+
minScale?: number;
|
|
6
|
+
maxScale?: number;
|
|
7
|
+
}
|
|
8
|
+
interface UseZoomReturn {
|
|
9
|
+
scale: number;
|
|
10
|
+
isPinching: boolean;
|
|
11
|
+
handleWheel: (event: ReactWheelEvent<Element>) => void;
|
|
12
|
+
handleTouchStart: (event: ReactTouchEvent<HTMLCanvasElement>) => boolean;
|
|
13
|
+
reset: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const useZoomPan: ({ displayWidth, displayHeight, minScale, maxScale, }?: UseZoomOptions) => UseZoomReturn;
|
|
16
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -368,6 +368,30 @@ interface RenderBuildRequest {
|
|
|
368
368
|
* ```
|
|
369
369
|
*/
|
|
370
370
|
height?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Render quality profile that controls visual effects and rendering speed.
|
|
373
|
+
*
|
|
374
|
+
* - **cinematic**: All effects enabled (shadows, ambient occlusion, bloom) for highest quality
|
|
375
|
+
* - **flat**: No effects for clean, simple product shots
|
|
376
|
+
* - **fast**: Minimal rendering for fastest processing speed
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```tsx
|
|
380
|
+
* const request: RenderBuildRequest = {
|
|
381
|
+
* parts: { CPU: ["7xjqsomhr"] },
|
|
382
|
+
* profile: 'cinematic' // High quality with all effects
|
|
383
|
+
* };
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* @example Fast rendering
|
|
387
|
+
* ```tsx
|
|
388
|
+
* const request: RenderBuildRequest = {
|
|
389
|
+
* parts: { CPU: ["7xjqsomhr"] },
|
|
390
|
+
* profile: 'fast' // Quick render, minimal effects
|
|
391
|
+
* };
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
profile?: 'cinematic' | 'flat' | 'fast';
|
|
371
395
|
}
|
|
372
396
|
/**
|
|
373
397
|
* Response structure containing all available parts for each category.
|
package/dist/index.esm.js
CHANGED
|
@@ -177,6 +177,8 @@ const renderBuildExperimental = async (request, config) => {
|
|
|
177
177
|
// Include width and height if provided
|
|
178
178
|
...(request.width !== undefined ? { width: request.width } : {}),
|
|
179
179
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
180
|
+
// Include profile if provided
|
|
181
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
180
182
|
};
|
|
181
183
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
182
184
|
method: "POST",
|
|
@@ -204,6 +206,8 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
204
206
|
// Include width and height if provided
|
|
205
207
|
...(request.width !== undefined ? { width: request.width } : {}),
|
|
206
208
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
209
|
+
// Include profile if provided
|
|
210
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
207
211
|
};
|
|
208
212
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
209
213
|
method: "POST",
|
|
@@ -267,6 +271,8 @@ const renderSpriteExperimental = async (request, config) => {
|
|
|
267
271
|
// Include width and height if provided
|
|
268
272
|
...(request.width !== undefined ? { width: request.width } : {}),
|
|
269
273
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
274
|
+
// Include profile if provided
|
|
275
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
270
276
|
};
|
|
271
277
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
272
278
|
method: "POST",
|
|
@@ -568,8 +574,101 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
568
574
|
} })) }));
|
|
569
575
|
};
|
|
570
576
|
|
|
577
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
578
|
+
const getTouchDistance = (touches) => {
|
|
579
|
+
const first = touches[0];
|
|
580
|
+
const second = touches[1];
|
|
581
|
+
if (!first || !second)
|
|
582
|
+
return 0;
|
|
583
|
+
return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY);
|
|
584
|
+
};
|
|
585
|
+
const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, } = {}) => {
|
|
586
|
+
const [scale, setScale] = useState(1);
|
|
587
|
+
const [isPinching, setIsPinching] = useState(false);
|
|
588
|
+
const scaleRef = useRef(1);
|
|
589
|
+
const pinchDataRef = useRef({
|
|
590
|
+
initialDistance: 0,
|
|
591
|
+
initialScale: 1,
|
|
592
|
+
});
|
|
593
|
+
const setScaleSafe = useCallback((next) => {
|
|
594
|
+
const clamped = clamp(next, minScale, maxScale);
|
|
595
|
+
if (clamped === scaleRef.current)
|
|
596
|
+
return;
|
|
597
|
+
scaleRef.current = clamped;
|
|
598
|
+
setScale(clamped);
|
|
599
|
+
}, [minScale, maxScale]);
|
|
600
|
+
const handleWheel = useCallback((event) => {
|
|
601
|
+
event.preventDefault();
|
|
602
|
+
event.stopPropagation();
|
|
603
|
+
const deltaY = event.deltaMode === 1
|
|
604
|
+
? event.deltaY * 16
|
|
605
|
+
: event.deltaMode === 2
|
|
606
|
+
? event.deltaY * (displayHeight ?? 300)
|
|
607
|
+
: event.deltaY;
|
|
608
|
+
const zoomFactor = Math.exp(-deltaY * 0.0015);
|
|
609
|
+
const nextScale = scaleRef.current * zoomFactor;
|
|
610
|
+
setScaleSafe(nextScale);
|
|
611
|
+
}, [setScaleSafe, displayHeight]);
|
|
612
|
+
const handleTouchStart = useCallback((event) => {
|
|
613
|
+
if (event.touches.length < 2) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const distance = getTouchDistance(event.touches);
|
|
617
|
+
if (!distance) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
pinchDataRef.current = {
|
|
621
|
+
initialDistance: distance,
|
|
622
|
+
initialScale: scaleRef.current,
|
|
623
|
+
};
|
|
624
|
+
setIsPinching(true);
|
|
625
|
+
event.preventDefault();
|
|
626
|
+
return true;
|
|
627
|
+
}, []);
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
if (!isPinching)
|
|
630
|
+
return;
|
|
631
|
+
const handleMove = (event) => {
|
|
632
|
+
if (event.touches.length < 2)
|
|
633
|
+
return;
|
|
634
|
+
const distance = getTouchDistance(event.touches);
|
|
635
|
+
if (!distance || pinchDataRef.current.initialDistance === 0)
|
|
636
|
+
return;
|
|
637
|
+
const scaleFactor = distance / pinchDataRef.current.initialDistance;
|
|
638
|
+
const nextScale = pinchDataRef.current.initialScale * scaleFactor;
|
|
639
|
+
setScaleSafe(nextScale);
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
};
|
|
642
|
+
const handleEnd = (event) => {
|
|
643
|
+
if (event.touches.length < 2) {
|
|
644
|
+
setIsPinching(false);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
window.addEventListener("touchmove", handleMove, { passive: false });
|
|
648
|
+
window.addEventListener("touchend", handleEnd);
|
|
649
|
+
window.addEventListener("touchcancel", handleEnd);
|
|
650
|
+
return () => {
|
|
651
|
+
window.removeEventListener("touchmove", handleMove);
|
|
652
|
+
window.removeEventListener("touchend", handleEnd);
|
|
653
|
+
window.removeEventListener("touchcancel", handleEnd);
|
|
654
|
+
};
|
|
655
|
+
}, [isPinching, setScaleSafe]);
|
|
656
|
+
const reset = useCallback(() => {
|
|
657
|
+
scaleRef.current = 1;
|
|
658
|
+
setScale(1);
|
|
659
|
+
}, []);
|
|
660
|
+
return {
|
|
661
|
+
scale,
|
|
662
|
+
isPinching,
|
|
663
|
+
handleWheel,
|
|
664
|
+
handleTouchStart,
|
|
665
|
+
reset,
|
|
666
|
+
};
|
|
667
|
+
};
|
|
668
|
+
|
|
571
669
|
const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
572
670
|
const canvasRef = useRef(null);
|
|
671
|
+
const containerRef = useRef(null);
|
|
573
672
|
const [img, setImg] = useState(null);
|
|
574
673
|
const [isLoading, setIsLoading] = useState(true);
|
|
575
674
|
const [bouncingAllowed, setBouncingAllowed] = useState(false);
|
|
@@ -582,6 +681,10 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
582
681
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
583
682
|
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
584
683
|
const frameRef = useRef(0);
|
|
684
|
+
const { scale, handleWheel: handleZoomWheel, handleTouchStart: handleZoomTouchStart, reset: resetZoom, } = useZoomPan({
|
|
685
|
+
displayWidth: displayW,
|
|
686
|
+
displayHeight: displayH,
|
|
687
|
+
});
|
|
585
688
|
// Image/frame sizes
|
|
586
689
|
const frameW = img ? img.width / cols : 0;
|
|
587
690
|
const frameH = img ? img.height / rows : 0;
|
|
@@ -640,8 +743,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
640
743
|
ctx.clearRect(0, 0, targetW, targetH);
|
|
641
744
|
ctx.imageSmoothingEnabled = true;
|
|
642
745
|
ctx.imageSmoothingQuality = "high";
|
|
643
|
-
|
|
644
|
-
|
|
746
|
+
const scaledW = targetW * scale;
|
|
747
|
+
const scaledH = targetH * scale;
|
|
748
|
+
const offsetX = -((scaledW - targetW) / 2);
|
|
749
|
+
const offsetY = -((scaledH - targetH) / 2);
|
|
750
|
+
ctx.drawImage(img, sx, sy, sw, sh, offsetX, offsetY, scaledW, scaledH);
|
|
751
|
+
}, [img, frameW, frameH, displayW, displayH, cols, total, scale]);
|
|
645
752
|
const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
|
|
646
753
|
mouseSensitivity,
|
|
647
754
|
touchSensitivity,
|
|
@@ -663,18 +770,66 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
663
770
|
frameRef.current = frame;
|
|
664
771
|
draw(frame);
|
|
665
772
|
}, [progressValue, hasDragged, img, total, draw]);
|
|
666
|
-
//
|
|
773
|
+
// Reset zoom when sprite changes or container size updates
|
|
774
|
+
useEffect(() => {
|
|
775
|
+
resetZoom();
|
|
776
|
+
}, [spriteSrc, displayW, displayH, resetZoom]);
|
|
777
|
+
// Add native wheel event listener to prevent scrolling AND handle zoom
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
const container = containerRef.current;
|
|
780
|
+
if (!container)
|
|
781
|
+
return;
|
|
782
|
+
const handleNativeWheel = (event) => {
|
|
783
|
+
event.preventDefault();
|
|
784
|
+
event.stopPropagation();
|
|
785
|
+
// Manually trigger zoom since we're preventing the React event
|
|
786
|
+
event.deltaMode === 1
|
|
787
|
+
? event.deltaY * 16
|
|
788
|
+
: event.deltaMode === 2
|
|
789
|
+
? event.deltaY * (displayH ?? 300)
|
|
790
|
+
: event.deltaY;
|
|
791
|
+
// We need to call the zoom handler directly
|
|
792
|
+
// Create a synthetic React event-like object
|
|
793
|
+
const syntheticEvent = {
|
|
794
|
+
preventDefault: () => { },
|
|
795
|
+
stopPropagation: () => { },
|
|
796
|
+
deltaY: event.deltaY,
|
|
797
|
+
deltaMode: event.deltaMode,
|
|
798
|
+
currentTarget: container,
|
|
799
|
+
};
|
|
800
|
+
handleZoomWheel(syntheticEvent);
|
|
801
|
+
hasDragged.current = true;
|
|
802
|
+
};
|
|
803
|
+
// Add listener to container to catch all wheel events
|
|
804
|
+
container.addEventListener('wheel', handleNativeWheel, { passive: false });
|
|
805
|
+
return () => {
|
|
806
|
+
container.removeEventListener('wheel', handleNativeWheel);
|
|
807
|
+
};
|
|
808
|
+
}, [handleZoomWheel, scale, displayH]);
|
|
809
|
+
// Initial draw once image is ready or zoom changes
|
|
667
810
|
useEffect(() => {
|
|
668
811
|
if (img && !isLoading) {
|
|
669
812
|
draw(frameRef.current);
|
|
670
813
|
}
|
|
671
814
|
}, [img, isLoading, draw]);
|
|
672
|
-
|
|
815
|
+
const handleCanvasTouchStart = useCallback((event) => {
|
|
816
|
+
if (handleZoomTouchStart(event)) {
|
|
817
|
+
hasDragged.current = true;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
handleTouchStart(event);
|
|
821
|
+
}, [handleZoomTouchStart, handleTouchStart, hasDragged]);
|
|
822
|
+
useCallback((event) => {
|
|
823
|
+
hasDragged.current = true;
|
|
824
|
+
handleZoomWheel(event);
|
|
825
|
+
}, [handleZoomWheel, hasDragged]);
|
|
826
|
+
return (jsxs("div", { ref: containerRef, style: {
|
|
673
827
|
position: "relative",
|
|
674
828
|
width: displayW,
|
|
675
829
|
height: displayH,
|
|
676
830
|
backgroundColor: "black",
|
|
677
|
-
|
|
831
|
+
overflow: "hidden",
|
|
832
|
+
}, children: [img && (jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleCanvasTouchStart, style: {
|
|
678
833
|
width: displayW,
|
|
679
834
|
height: displayH,
|
|
680
835
|
cursor: isDragging ? "grabbing" : "grab",
|