@buildcores/render-client 1.3.0 → 1.4.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/api.d.ts +86 -1
- package/dist/hooks/useSpriteRender.d.ts +28 -1
- package/dist/index.d.ts +297 -5
- package/dist/index.esm.js +264 -15
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +266 -13
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +182 -2
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
import { useAnimationFrame } from 'framer-motion';
|
|
4
4
|
|
|
5
5
|
// Helper to extract clientX from mouse or touch events
|
|
@@ -147,6 +147,9 @@ const API_ENDPOINTS = {
|
|
|
147
147
|
RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental",
|
|
148
148
|
RENDER_BUILD: "/render-build",
|
|
149
149
|
AVAILABLE_PARTS: "/available-parts",
|
|
150
|
+
BUILD: "/build",
|
|
151
|
+
PARTS: "/parts",
|
|
152
|
+
RENDER_BY_SHARE_CODE: "/render-by-share-code",
|
|
150
153
|
};
|
|
151
154
|
// API URL helpers
|
|
152
155
|
const buildApiUrl = (endpoint, config) => {
|
|
@@ -208,6 +211,10 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
208
211
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
209
212
|
// Include profile if provided
|
|
210
213
|
...(request.profile ? { profile: request.profile } : {}),
|
|
214
|
+
// Include composition settings
|
|
215
|
+
...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
|
|
216
|
+
...(request.cameraOffsetX !== undefined ? { cameraOffsetX: request.cameraOffsetX } : {}),
|
|
217
|
+
...(request.gridSettings ? { gridSettings: request.gridSettings } : {}),
|
|
211
218
|
};
|
|
212
219
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
213
220
|
method: "POST",
|
|
@@ -313,6 +320,169 @@ const getAvailableParts = async (category, config, options) => {
|
|
|
313
320
|
}
|
|
314
321
|
return (await response.json());
|
|
315
322
|
};
|
|
323
|
+
// ============================================
|
|
324
|
+
// Build and Parts API Functions
|
|
325
|
+
// ============================================
|
|
326
|
+
/**
|
|
327
|
+
* Fetch a build by its share code.
|
|
328
|
+
* Returns build metadata and parts organized by category.
|
|
329
|
+
*
|
|
330
|
+
* @param shareCode - The share code of the build to fetch
|
|
331
|
+
* @param config - API configuration (environment, auth token)
|
|
332
|
+
* @returns Promise with build details including parts
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```tsx
|
|
336
|
+
* const build = await getBuildByShareCode('abc123xyz', {
|
|
337
|
+
* environment: 'prod',
|
|
338
|
+
* authToken: 'your-api-key'
|
|
339
|
+
* });
|
|
340
|
+
*
|
|
341
|
+
* console.log(build.name); // "My Gaming PC"
|
|
342
|
+
* console.log(build.parts.CPU); // ["7xjqsomhr"]
|
|
343
|
+
*
|
|
344
|
+
* // Use the parts directly with BuildRender
|
|
345
|
+
* <BuildRender parts={{ parts: build.parts }} size={500} apiConfig={config} />
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
const getBuildByShareCode = async (shareCode, config) => {
|
|
349
|
+
const url = buildApiUrl(`${API_ENDPOINTS.BUILD}/${encodeURIComponent(shareCode)}`, config);
|
|
350
|
+
const response = await fetch(url, {
|
|
351
|
+
method: "GET",
|
|
352
|
+
headers: buildHeaders(config),
|
|
353
|
+
});
|
|
354
|
+
if (response.status === 404) {
|
|
355
|
+
throw new Error("Build not found");
|
|
356
|
+
}
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
throw new Error(`Get build by share code failed: ${response.status} ${response.statusText}`);
|
|
359
|
+
}
|
|
360
|
+
return (await response.json());
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Fetch part details by their BuildCores IDs.
|
|
364
|
+
*
|
|
365
|
+
* @param partIds - Array of BuildCores part IDs to fetch
|
|
366
|
+
* @param config - API configuration (environment, auth token)
|
|
367
|
+
* @returns Promise with part details
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```tsx
|
|
371
|
+
* const response = await getPartsByIds(['7xjqsomhr', 'z7pyphm9k'], {
|
|
372
|
+
* environment: 'prod',
|
|
373
|
+
* authToken: 'your-api-key'
|
|
374
|
+
* });
|
|
375
|
+
*
|
|
376
|
+
* response.parts.forEach(part => {
|
|
377
|
+
* console.log(`${part.name} (${part.category})`);
|
|
378
|
+
* });
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
const getPartsByIds = async (partIds, config) => {
|
|
382
|
+
const url = buildApiUrl(API_ENDPOINTS.PARTS, config);
|
|
383
|
+
const response = await fetch(url, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: buildHeaders(config),
|
|
386
|
+
body: JSON.stringify({ ids: partIds }),
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new Error(`Get parts by IDs failed: ${response.status} ${response.statusText}`);
|
|
390
|
+
}
|
|
391
|
+
return (await response.json());
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Create a render job for a build by its share code.
|
|
395
|
+
* Returns the job ID for polling status.
|
|
396
|
+
*
|
|
397
|
+
* @param shareCode - The share code of the build to render
|
|
398
|
+
* @param config - API configuration (environment, auth token)
|
|
399
|
+
* @param options - Render options (format, dimensions, profile)
|
|
400
|
+
* @returns Promise with job creation response
|
|
401
|
+
*/
|
|
402
|
+
const createRenderByShareCodeJob = async (shareCode, config, options) => {
|
|
403
|
+
const url = buildApiUrl(API_ENDPOINTS.RENDER_BY_SHARE_CODE, config);
|
|
404
|
+
const body = {
|
|
405
|
+
shareCode,
|
|
406
|
+
...(options?.format ? { format: options.format } : {}),
|
|
407
|
+
...(options?.width !== undefined ? { width: options.width } : {}),
|
|
408
|
+
...(options?.height !== undefined ? { height: options.height } : {}),
|
|
409
|
+
...(options?.profile ? { profile: options.profile } : {}),
|
|
410
|
+
...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
|
|
411
|
+
...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
|
|
412
|
+
...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
|
|
413
|
+
};
|
|
414
|
+
const response = await fetch(url, {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers: buildHeaders(config),
|
|
417
|
+
body: JSON.stringify(body),
|
|
418
|
+
});
|
|
419
|
+
if (response.status === 404) {
|
|
420
|
+
throw new Error("Build not found");
|
|
421
|
+
}
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
|
|
424
|
+
}
|
|
425
|
+
const data = (await response.json());
|
|
426
|
+
if (!data?.job_id) {
|
|
427
|
+
throw new Error("Create render job failed: missing job_id in response");
|
|
428
|
+
}
|
|
429
|
+
return data;
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Render a build by its share code.
|
|
433
|
+
* This is a convenience function that creates a render job and polls until completion.
|
|
434
|
+
*
|
|
435
|
+
* @param shareCode - The share code of the build to render
|
|
436
|
+
* @param config - API configuration (environment, auth token)
|
|
437
|
+
* @param options - Render options including polling configuration
|
|
438
|
+
* @returns Promise with the final render URL
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* // Simple usage - just pass share code
|
|
443
|
+
* const result = await renderByShareCode('abc123xyz', {
|
|
444
|
+
* environment: 'prod',
|
|
445
|
+
* authToken: 'your-api-key'
|
|
446
|
+
* });
|
|
447
|
+
* console.log(result.videoUrl); // URL to rendered video
|
|
448
|
+
*
|
|
449
|
+
* // With custom options
|
|
450
|
+
* const result = await renderByShareCode('abc123xyz', config, {
|
|
451
|
+
* format: 'sprite',
|
|
452
|
+
* width: 1920,
|
|
453
|
+
* height: 1080,
|
|
454
|
+
* profile: 'cinematic',
|
|
455
|
+
* timeoutMs: 180000 // 3 minute timeout
|
|
456
|
+
* });
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
const renderByShareCode = async (shareCode, config, options) => {
|
|
460
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 1500;
|
|
461
|
+
const timeoutMs = options?.timeoutMs ?? 120000; // 2 minutes default
|
|
462
|
+
const { job_id } = await createRenderByShareCodeJob(shareCode, config, options);
|
|
463
|
+
const start = Date.now();
|
|
464
|
+
// Poll until completed or error or timeout
|
|
465
|
+
for (;;) {
|
|
466
|
+
const status = await getRenderBuildStatus(job_id, config);
|
|
467
|
+
if (status.status === "completed") {
|
|
468
|
+
const requestedFormat = options?.format ?? "video";
|
|
469
|
+
const finalUrl = requestedFormat === "sprite"
|
|
470
|
+
? status.sprite_url || status.url || undefined
|
|
471
|
+
: status.video_url || status.url || undefined;
|
|
472
|
+
if (!finalUrl) {
|
|
473
|
+
throw new Error("Render job completed but no URL returned");
|
|
474
|
+
}
|
|
475
|
+
return { videoUrl: finalUrl };
|
|
476
|
+
}
|
|
477
|
+
if (status.status === "error") {
|
|
478
|
+
throw new Error(status.error || "Render job failed");
|
|
479
|
+
}
|
|
480
|
+
if (Date.now() - start > timeoutMs) {
|
|
481
|
+
throw new Error("Timed out waiting for render job to complete");
|
|
482
|
+
}
|
|
483
|
+
await sleep(pollIntervalMs);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
316
486
|
|
|
317
487
|
/**
|
|
318
488
|
* Enum defining all available PC part categories that can be rendered.
|
|
@@ -353,6 +523,8 @@ var PartCategory;
|
|
|
353
523
|
PartCategory["PCCase"] = "PCCase";
|
|
354
524
|
/** CPU Cooler - Air or liquid cooling for the processor */
|
|
355
525
|
PartCategory["CPUCooler"] = "CPUCooler";
|
|
526
|
+
/** Case Fans - Additional cooling fans for the case */
|
|
527
|
+
PartCategory["CaseFan"] = "CaseFan";
|
|
356
528
|
})(PartCategory || (PartCategory = {}));
|
|
357
529
|
|
|
358
530
|
/**
|
|
@@ -445,20 +617,47 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
445
617
|
};
|
|
446
618
|
};
|
|
447
619
|
|
|
448
|
-
const useSpriteRender = (
|
|
620
|
+
const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
449
621
|
const [spriteSrc, setSpriteSrc] = useState(null);
|
|
450
622
|
const [isRenderingSprite, setIsRenderingSprite] = useState(false);
|
|
451
623
|
const [renderError, setRenderError] = useState(null);
|
|
452
624
|
const [spriteMetadata, setSpriteMetadata] = useState(null);
|
|
453
|
-
const
|
|
454
|
-
|
|
625
|
+
const previousInputRef = useRef(null);
|
|
626
|
+
// Normalize input to SpriteRenderInput format
|
|
627
|
+
const normalizedInput = 'type' in input ? input : { type: 'parts', parts: input };
|
|
628
|
+
const fetchRenderSprite = useCallback(async (currentInput) => {
|
|
455
629
|
try {
|
|
456
630
|
setIsRenderingSprite(true);
|
|
457
631
|
setRenderError(null);
|
|
458
632
|
onLoadStart?.();
|
|
633
|
+
// Handle share code rendering - uses existing build with proper interactive state
|
|
634
|
+
if (currentInput.type === 'shareCode') {
|
|
635
|
+
const { videoUrl: spriteUrl } = await renderByShareCode(currentInput.shareCode, apiConfig, {
|
|
636
|
+
format: 'sprite',
|
|
637
|
+
profile: currentInput.profile,
|
|
638
|
+
showGrid: currentInput.showGrid,
|
|
639
|
+
cameraOffsetX: currentInput.cameraOffsetX,
|
|
640
|
+
gridSettings: currentInput.gridSettings
|
|
641
|
+
});
|
|
642
|
+
setSpriteSrc((prevSrc) => {
|
|
643
|
+
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
644
|
+
URL.revokeObjectURL(prevSrc);
|
|
645
|
+
}
|
|
646
|
+
return spriteUrl;
|
|
647
|
+
});
|
|
648
|
+
setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// Handle parts-based rendering (creates new build)
|
|
652
|
+
const currentParts = currentInput.parts;
|
|
459
653
|
const mode = options?.mode ?? "async";
|
|
460
654
|
if (mode === "experimental") {
|
|
461
|
-
const response = await renderSpriteExperimental(
|
|
655
|
+
const response = await renderSpriteExperimental({
|
|
656
|
+
...currentParts,
|
|
657
|
+
showGrid: currentInput.showGrid,
|
|
658
|
+
cameraOffsetX: currentInput.cameraOffsetX,
|
|
659
|
+
gridSettings: currentInput.gridSettings,
|
|
660
|
+
}, apiConfig);
|
|
462
661
|
const objectUrl = URL.createObjectURL(response.sprite);
|
|
463
662
|
// Clean up previous sprite URL before setting new one
|
|
464
663
|
setSpriteSrc((prevSrc) => {
|
|
@@ -476,7 +675,13 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
476
675
|
}
|
|
477
676
|
else {
|
|
478
677
|
// Async job-based flow: request sprite format and use returned URL
|
|
479
|
-
const { videoUrl: spriteUrl } = await renderBuild({
|
|
678
|
+
const { videoUrl: spriteUrl } = await renderBuild({
|
|
679
|
+
...currentParts,
|
|
680
|
+
format: "sprite",
|
|
681
|
+
showGrid: currentInput.showGrid,
|
|
682
|
+
cameraOffsetX: currentInput.cameraOffsetX,
|
|
683
|
+
gridSettings: currentInput.gridSettings,
|
|
684
|
+
}, apiConfig);
|
|
480
685
|
setSpriteSrc((prevSrc) => {
|
|
481
686
|
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
482
687
|
URL.revokeObjectURL(prevSrc);
|
|
@@ -494,15 +699,39 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
494
699
|
setIsRenderingSprite(false);
|
|
495
700
|
}
|
|
496
701
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
497
|
-
//
|
|
702
|
+
// Check if inputs are equal
|
|
703
|
+
const areInputsEqual = (a, b) => {
|
|
704
|
+
if (!a)
|
|
705
|
+
return false;
|
|
706
|
+
if (a.type !== b.type)
|
|
707
|
+
return false;
|
|
708
|
+
if (a.type === 'shareCode' && b.type === 'shareCode') {
|
|
709
|
+
// Compare grid settings (shallow comparison of properties)
|
|
710
|
+
const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
|
|
711
|
+
return a.shareCode === b.shareCode &&
|
|
712
|
+
a.profile === b.profile &&
|
|
713
|
+
a.showGrid === b.showGrid &&
|
|
714
|
+
a.cameraOffsetX === b.cameraOffsetX &&
|
|
715
|
+
gridSettingsEqual;
|
|
716
|
+
}
|
|
717
|
+
if (a.type === 'parts' && b.type === 'parts') {
|
|
718
|
+
// Compare grid settings (shallow comparison of properties)
|
|
719
|
+
const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
|
|
720
|
+
return arePartsEqual(a.parts, b.parts) &&
|
|
721
|
+
a.showGrid === b.showGrid &&
|
|
722
|
+
a.cameraOffsetX === b.cameraOffsetX &&
|
|
723
|
+
gridSettingsEqual;
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
};
|
|
727
|
+
// Effect to call API when input changes
|
|
498
728
|
useEffect(() => {
|
|
499
|
-
const shouldFetch =
|
|
500
|
-
!arePartsEqual(previousPartsRef.current, parts);
|
|
729
|
+
const shouldFetch = !areInputsEqual(previousInputRef.current, normalizedInput);
|
|
501
730
|
if (shouldFetch) {
|
|
502
|
-
|
|
503
|
-
fetchRenderSprite(
|
|
731
|
+
previousInputRef.current = normalizedInput;
|
|
732
|
+
fetchRenderSprite(normalizedInput);
|
|
504
733
|
}
|
|
505
|
-
}, [
|
|
734
|
+
}, [normalizedInput, fetchRenderSprite]);
|
|
506
735
|
// Cleanup effect for component unmount
|
|
507
736
|
useEffect(() => {
|
|
508
737
|
return () => {
|
|
@@ -666,7 +895,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, }
|
|
|
666
895
|
};
|
|
667
896
|
};
|
|
668
897
|
|
|
669
|
-
const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
898
|
+
const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, gridSettings, }) => {
|
|
670
899
|
const canvasRef = useRef(null);
|
|
671
900
|
const containerRef = useRef(null);
|
|
672
901
|
const [img, setImg] = useState(null);
|
|
@@ -674,8 +903,28 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
674
903
|
const [bouncingAllowed, setBouncingAllowed] = useState(false);
|
|
675
904
|
const displayW = width ?? size ?? 300;
|
|
676
905
|
const displayH = height ?? size ?? 300;
|
|
906
|
+
// Build the render input - prefer shareCode if provided (preserves interactive state like case fan slots)
|
|
907
|
+
const renderInput = useMemo(() => {
|
|
908
|
+
if (shareCode) {
|
|
909
|
+
return {
|
|
910
|
+
type: 'shareCode',
|
|
911
|
+
shareCode,
|
|
912
|
+
profile: parts?.profile,
|
|
913
|
+
showGrid,
|
|
914
|
+
cameraOffsetX,
|
|
915
|
+
gridSettings,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
type: 'parts',
|
|
920
|
+
parts: parts,
|
|
921
|
+
showGrid,
|
|
922
|
+
cameraOffsetX,
|
|
923
|
+
gridSettings,
|
|
924
|
+
};
|
|
925
|
+
}, [shareCode, parts, showGrid, cameraOffsetX, gridSettings]);
|
|
677
926
|
// Use custom hook for sprite rendering
|
|
678
|
-
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(
|
|
927
|
+
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
|
|
679
928
|
const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
|
|
680
929
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
681
930
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
@@ -998,5 +1247,5 @@ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRende
|
|
|
998
1247
|
!hasDragged.current, progressValue: progressValue })] }));
|
|
999
1248
|
};
|
|
1000
1249
|
|
|
1001
|
-
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 };
|
|
1250
|
+
export { API_BASE_URL, API_ENDPOINTS, BuildRender, BuildRenderVideo, DragIcon, InstructionTooltip, LoadingErrorOverlay, PartCategory, arePartsEqual, buildApiUrl, buildHeaders, calculateCircularFrame, calculateCircularTime, createRenderByShareCodeJob, getAvailableParts, getBuildByShareCode, getPartsByIds, renderBuildExperimental, renderByShareCode, renderSpriteExperimental, useBouncePatternProgress, useBuildRender, useSpriteRender, useSpriteScrubbing, useVideoScrubbing };
|
|
1002
1251
|
//# sourceMappingURL=index.esm.js.map
|