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