@buildcores/render-client 1.0.8 → 1.0.10

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/api.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface RenderJobStatusResponse {
27
27
  job_id: string;
28
28
  status: "queued" | "processing" | "completed" | "error";
29
29
  url?: string | null;
30
+ video_url?: string | null;
31
+ sprite_url?: string | null;
30
32
  error?: string | null;
31
33
  end_time?: string | null;
32
34
  }
@@ -9,4 +9,12 @@ export interface UseSpriteRenderReturn {
9
9
  totalFrames: number;
10
10
  } | null;
11
11
  }
12
- export declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void) => UseSpriteRenderReturn;
12
+ export interface UseSpriteRenderOptions {
13
+ /**
14
+ * Choose which backend flow to use
15
+ * - 'async' (default): uses /render-build and polls /render-build/{jobId} with format 'sprite'
16
+ * - 'experimental': uses /render-build-experimental and returns Blob
17
+ */
18
+ mode?: "async" | "experimental";
19
+ }
20
+ export declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseSpriteRenderOptions) => UseSpriteRenderReturn;
package/dist/index.d.ts CHANGED
@@ -45,24 +45,12 @@ interface BuildRenderVideoProps {
45
45
  */
46
46
  parts: RenderBuildRequest;
47
47
  /**
48
- * Video size in pixels (width and height will be the same).
49
- *
50
- * This determines the resolution of the rendered 3D video. Higher values
51
- * provide better quality but may impact performance.
52
- *
53
- * @example
54
- * ```tsx
55
- * <BuildRender parts={parts} size={300} /> // 300x300px
56
- * <BuildRender parts={parts} size={500} /> // 500x500px
57
- * <BuildRender parts={parts} size={800} /> // 800x800px - high quality
58
- * ```
59
- *
60
- * Recommended sizes:
61
- * - 300px: Good for thumbnails or small previews
62
- * - 500px: Standard size for most use cases
63
- * - 800px+: High quality for detailed viewing
48
+ * Width and height in pixels. If only `size` is provided, both width and height use it.
49
+ * If `width`/`height` are provided, they override `size` individually.
64
50
  */
65
- size: number;
51
+ width?: number;
52
+ height?: number;
53
+ size?: number;
66
54
  /**
67
55
  * API configuration for environment and authentication.
68
56
  * This is required to make API calls to the BuildCores rendering service.
@@ -159,19 +147,12 @@ interface BuildRenderProps {
159
147
  */
160
148
  parts: RenderBuildRequest;
161
149
  /**
162
- * Sprite size in pixels (width and height will be the same).
163
- *
164
- * This determines the display size of the rendered 3D sprite. The sprite sheet
165
- * itself is rendered at a fixed resolution, but this controls the display size.
166
- *
167
- * @example
168
- * ```tsx
169
- * <SpriteRender parts={parts} size={300} /> // 300x300px
170
- * <SpriteRender parts={parts} size={500} /> // 500x500px
171
- * <SpriteRender parts={parts} size={800} /> // 800x800px - larger display
172
- * ```
150
+ * Width and height in pixels. If only `size` is provided, both width and height use it.
151
+ * If `width`/`height` are provided, they override `size` individually.
173
152
  */
174
- size: number;
153
+ width?: number;
154
+ height?: number;
155
+ size?: number;
175
156
  /**
176
157
  * API configuration for environment and authentication.
177
158
  * This is required to make API calls to the BuildCores rendering service.
@@ -475,7 +456,15 @@ interface UseSpriteRenderReturn {
475
456
  totalFrames: number;
476
457
  } | null;
477
458
  }
478
- declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void) => UseSpriteRenderReturn;
459
+ interface UseSpriteRenderOptions {
460
+ /**
461
+ * Choose which backend flow to use
462
+ * - 'async' (default): uses /render-build and polls /render-build/{jobId} with format 'sprite'
463
+ * - 'experimental': uses /render-build-experimental and returns Blob
464
+ */
465
+ mode?: "async" | "experimental";
466
+ }
467
+ declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseSpriteRenderOptions) => UseSpriteRenderReturn;
479
468
 
480
469
  interface DragIconProps {
481
470
  width?: number;
@@ -557,4 +546,4 @@ declare const renderSpriteExperimental: (request: RenderBuildRequest, config: Ap
557
546
  declare const getAvailableParts: (config: ApiConfig) => Promise<AvailablePartsResponse>;
558
547
 
559
548
  export { API_BASE_URL, API_ENDPOINTS, BuildRender, BuildRenderVideo, DragIcon, InstructionTooltip, LoadingErrorOverlay, PartCategory, arePartsEqual, buildApiUrl, buildHeaders, calculateCircularFrame, calculateCircularTime, getAvailableParts, renderBuildExperimental, renderSpriteExperimental, useBouncePatternProgress, useBuildRender, useSpriteRender, useSpriteScrubbing, useVideoScrubbing };
560
- export type { ApiConfig, AvailablePartsResponse, BuildRenderProps, BuildRenderVideoProps, PartDetails, RenderAPIService, RenderBuildRequest, RenderBuildResponse, RenderSpriteResponse, UseBuildRenderOptions, UseBuildRenderReturn, UseSpriteRenderReturn };
549
+ export type { ApiConfig, AvailablePartsResponse, BuildRenderProps, BuildRenderVideoProps, PartDetails, RenderAPIService, RenderBuildRequest, RenderBuildResponse, RenderSpriteResponse, UseBuildRenderOptions, UseBuildRenderReturn, UseSpriteRenderOptions, UseSpriteRenderReturn };
package/dist/index.esm.js CHANGED
@@ -415,7 +415,10 @@ const renderBuild = async (request, config, options) => {
415
415
  for (;;) {
416
416
  const status = await getRenderBuildStatus(job_id, config);
417
417
  if (status.status === "completed") {
418
- const finalUrl = status.url ?? undefined;
418
+ const requestedFormat = request.format ?? "video";
419
+ const finalUrl = (requestedFormat === "sprite"
420
+ ? status.sprite_url || status.url || undefined
421
+ : status.video_url || status.url || undefined);
419
422
  if (!finalUrl) {
420
423
  throw new Error("Render job completed but no URL returned");
421
424
  }
@@ -597,7 +600,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
597
600
  };
598
601
  };
599
602
 
600
- const useSpriteRender = (parts, apiConfig, onLoadStart) => {
603
+ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
601
604
  const [spriteSrc, setSpriteSrc] = useState(null);
602
605
  const [isRenderingSprite, setIsRenderingSprite] = useState(false);
603
606
  const [renderError, setRenderError] = useState(null);
@@ -608,21 +611,36 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
608
611
  setIsRenderingSprite(true);
609
612
  setRenderError(null);
610
613
  onLoadStart?.();
611
- const response = await renderSpriteExperimental(currentParts, apiConfig);
612
- const objectUrl = URL.createObjectURL(response.sprite);
613
- // Clean up previous sprite URL before setting new one
614
- setSpriteSrc((prevSrc) => {
615
- if (prevSrc) {
616
- URL.revokeObjectURL(prevSrc);
617
- }
618
- return objectUrl;
619
- });
620
- // Set sprite metadata
621
- setSpriteMetadata({
622
- cols: response.metadata?.cols || 12,
623
- rows: response.metadata?.rows || 6,
624
- totalFrames: response.metadata?.totalFrames || 72,
625
- });
614
+ const mode = options?.mode ?? "async";
615
+ if (mode === "experimental") {
616
+ const response = await renderSpriteExperimental(currentParts, apiConfig);
617
+ const objectUrl = URL.createObjectURL(response.sprite);
618
+ // Clean up previous sprite URL before setting new one
619
+ setSpriteSrc((prevSrc) => {
620
+ if (prevSrc && prevSrc.startsWith("blob:")) {
621
+ URL.revokeObjectURL(prevSrc);
622
+ }
623
+ return objectUrl;
624
+ });
625
+ // Set sprite metadata
626
+ setSpriteMetadata({
627
+ cols: response.metadata?.cols || 12,
628
+ rows: response.metadata?.rows || 6,
629
+ totalFrames: response.metadata?.totalFrames || 72,
630
+ });
631
+ }
632
+ else {
633
+ // Async job-based flow: request sprite format and use returned URL
634
+ const { videoUrl: spriteUrl } = await renderBuild({ ...currentParts, format: "sprite" }, apiConfig);
635
+ setSpriteSrc((prevSrc) => {
636
+ if (prevSrc && prevSrc.startsWith("blob:")) {
637
+ URL.revokeObjectURL(prevSrc);
638
+ }
639
+ return spriteUrl;
640
+ });
641
+ // No metadata from async endpoint; keep defaults
642
+ setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
643
+ }
626
644
  }
627
645
  catch (error) {
628
646
  setRenderError(error instanceof Error ? error.message : "Failed to render sprite");
@@ -630,7 +648,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
630
648
  finally {
631
649
  setIsRenderingSprite(false);
632
650
  }
633
- }, [apiConfig, onLoadStart]);
651
+ }, [apiConfig, onLoadStart, options?.mode]);
634
652
  // Effect to call API when parts content changes (using custom equality check)
635
653
  useEffect(() => {
636
654
  const shouldFetch = previousPartsRef.current === null ||
@@ -643,7 +661,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
643
661
  // Cleanup effect for component unmount
644
662
  useEffect(() => {
645
663
  return () => {
646
- if (spriteSrc) {
664
+ if (spriteSrc && spriteSrc.startsWith("blob:")) {
647
665
  URL.revokeObjectURL(spriteSrc);
648
666
  }
649
667
  };
@@ -711,11 +729,13 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
711
729
  } })) }));
712
730
  };
713
731
 
714
- const BuildRender = ({ parts, size, apiConfig, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
732
+ const BuildRender = ({ parts, width, height, size, apiConfig, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
715
733
  const canvasRef = useRef(null);
716
734
  const [img, setImg] = useState(null);
717
735
  const [isLoading, setIsLoading] = useState(true);
718
736
  const [bouncingAllowed, setBouncingAllowed] = useState(false);
737
+ const displayW = width ?? size ?? 300;
738
+ const displayH = height ?? size ?? 300;
719
739
  // Use custom hook for sprite rendering
720
740
  const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(parts, apiConfig);
721
741
  const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
@@ -761,8 +781,8 @@ const BuildRender = ({ parts, size, apiConfig, mouseSensitivity = 0.2, touchSens
761
781
  return;
762
782
  // Backing store sized for HiDPI; CSS size stays `size`
763
783
  const dpr = Math.max(1, window.devicePixelRatio || 1);
764
- const targetW = Math.round(size * dpr);
765
- const targetH = Math.round(size * dpr);
784
+ const targetW = Math.round(displayW * dpr);
785
+ const targetH = Math.round(displayH * dpr);
766
786
  if (cnv.width !== targetW || cnv.height !== targetH) {
767
787
  cnv.width = targetW;
768
788
  cnv.height = targetH;
@@ -782,7 +802,7 @@ const BuildRender = ({ parts, size, apiConfig, mouseSensitivity = 0.2, touchSens
782
802
  ctx.imageSmoothingEnabled = true;
783
803
  ctx.imageSmoothingQuality = "high";
784
804
  ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
785
- }, [img, frameW, frameH, size, cols, total]);
805
+ }, [img, frameW, frameH, displayW, displayH, cols, total]);
786
806
  const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
787
807
  mouseSensitivity,
788
808
  touchSensitivity,
@@ -812,19 +832,19 @@ const BuildRender = ({ parts, size, apiConfig, mouseSensitivity = 0.2, touchSens
812
832
  }, [img, isLoading, draw]);
813
833
  return (jsxs("div", { style: {
814
834
  position: "relative",
815
- width: size,
816
- height: size,
835
+ width: displayW,
836
+ height: displayH,
817
837
  backgroundColor: "black",
818
838
  }, children: [img && (jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, style: {
819
- width: size,
820
- height: size,
839
+ width: displayW,
840
+ height: displayH,
821
841
  cursor: isDragging ? "grabbing" : "grab",
822
842
  touchAction: "none", // Prevents default touch behaviors like scrolling
823
843
  display: "block",
824
844
  userSelect: "none",
825
845
  WebkitUserSelect: "none",
826
846
  WebkitTouchCallout: "none",
827
- }, role: "img", "aria-label": "360\u00B0 viewer", onContextMenu: (e) => e.preventDefault() })), jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingSprite || !!renderError, renderError: renderError || undefined, size: size }), jsx(InstructionTooltip, { isVisible: !isLoading &&
847
+ }, role: "img", "aria-label": "360\u00B0 viewer", onContextMenu: (e) => e.preventDefault() })), jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingSprite || !!renderError, renderError: renderError || undefined, size: Math.min(displayW, displayH) }), jsx(InstructionTooltip, { isVisible: !isLoading &&
828
848
  !isRenderingSprite &&
829
849
  !renderError &&
830
850
  isBouncing &&
@@ -924,10 +944,12 @@ const useVideoScrubbing = (videoRef, options = {}) => {
924
944
  };
925
945
  };
926
946
 
927
- const BuildRenderVideo = ({ parts, size, apiConfig, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
947
+ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
928
948
  const videoRef = useRef(null);
929
949
  const [isLoading, setIsLoading] = useState(true);
930
950
  const [bouncingAllowed, setBouncingAllowed] = useState(false);
951
+ const displayW = width ?? size ?? 300;
952
+ const displayH = height ?? size ?? 300;
931
953
  // Use custom hook for build rendering
932
954
  const { videoSrc, isRenderingBuild, renderError } = useBuildRender(parts, apiConfig);
933
955
  const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
@@ -957,7 +979,7 @@ const BuildRenderVideo = ({ parts, size, apiConfig, mouseSensitivity = 0.01, tou
957
979
  videoRef.current.currentTime = time;
958
980
  }
959
981
  }, [progressValue, hasDragged]);
960
- return (jsxs("div", { style: { position: "relative", width: size, height: size }, children: [videoSrc && (jsx("video", { ref: videoRef, src: videoSrc, width: size, height: size, autoPlay: true, preload: "metadata", muted: true, playsInline: true, controls: false, disablePictureInPicture: true, controlsList: "nodownload nofullscreen noremoteplayback", ...{ "x-webkit-airplay": "deny" }, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onLoadStart: handleLoadStartInternal, onCanPlay: handleCanPlayInternal, onLoadedData: () => {
982
+ return (jsxs("div", { style: { position: "relative", width: displayW, height: displayH }, children: [videoSrc && (jsx("video", { ref: videoRef, src: videoSrc, width: displayW, height: displayH, autoPlay: true, preload: "metadata", muted: true, playsInline: true, controls: false, disablePictureInPicture: true, controlsList: "nodownload nofullscreen noremoteplayback", ...{ "x-webkit-airplay": "deny" }, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onLoadStart: handleLoadStartInternal, onCanPlay: handleCanPlayInternal, onLoadedData: () => {
961
983
  if (videoRef.current) {
962
984
  videoRef.current.pause();
963
985
  }
@@ -975,7 +997,7 @@ const BuildRenderVideo = ({ parts, size, apiConfig, mouseSensitivity = 0.01, tou
975
997
  WebkitTouchCallout: "none",
976
998
  WebkitUserSelect: "none",
977
999
  userSelect: "none",
978
- }, children: "Your browser does not support the video tag." }, videoSrc)), jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingBuild || !!renderError, renderError: renderError || undefined, size: size }), jsx(InstructionTooltip, { isVisible: !isLoading &&
1000
+ }, children: "Your browser does not support the video tag." }, videoSrc)), jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingBuild || !!renderError, renderError: renderError || undefined, size: Math.min(displayW, displayH) }), jsx(InstructionTooltip, { isVisible: !isLoading &&
979
1001
  !isRenderingBuild &&
980
1002
  !renderError &&
981
1003
  isBouncing &&