@buildcores/render-client 1.5.0 → 1.7.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/index.esm.js CHANGED
@@ -211,17 +211,73 @@ const buildApiUrl = (endpoint, config) => {
211
211
  }
212
212
  return baseUrl;
213
213
  };
214
+ const SESSION_TOKEN_REFRESH_WINDOW_MS = 15000;
215
+ const sessionTokenCache = new WeakMap();
216
+ const resolveAuthMode = (config) => config.authMode ?? (config.getRenderSessionToken ? "session" : "legacy");
217
+ const isSessionAuthMode = (config) => resolveAuthMode(config) === "session";
218
+ const parseExpiresAtMs = (expiresAt) => {
219
+ const parsed = Date.parse(expiresAt);
220
+ if (!Number.isFinite(parsed)) {
221
+ throw new Error("Invalid session token expiry (expiresAt)");
222
+ }
223
+ return parsed;
224
+ };
225
+ const resolveSessionToken = async (config, forceRefresh) => {
226
+ const supplier = config.getRenderSessionToken;
227
+ if (!supplier) {
228
+ throw new Error("authMode=session requires getRenderSessionToken");
229
+ }
230
+ const cached = sessionTokenCache.get(supplier);
231
+ const now = Date.now();
232
+ if (!forceRefresh &&
233
+ cached &&
234
+ cached.expiresAtMs - SESSION_TOKEN_REFRESH_WINDOW_MS > now) {
235
+ return cached.token;
236
+ }
237
+ const session = await supplier();
238
+ if (!session?.token || !session?.expiresAt) {
239
+ throw new Error("getRenderSessionToken must return { token, expiresAt }");
240
+ }
241
+ const expiresAtMs = parseExpiresAtMs(session.expiresAt);
242
+ sessionTokenCache.set(supplier, {
243
+ token: session.token,
244
+ expiresAtMs,
245
+ });
246
+ return session.token;
247
+ };
248
+ const resolveAuthToken = async (config, forceRefresh) => {
249
+ if (isSessionAuthMode(config)) {
250
+ return resolveSessionToken(config, forceRefresh);
251
+ }
252
+ return config.authToken;
253
+ };
214
254
  // Helper to build request headers with auth token
215
- const buildHeaders = (config) => {
255
+ const buildHeaders = (config, authTokenOverride) => {
216
256
  const headers = {
217
257
  "Content-Type": "application/json",
218
258
  accept: "application/json",
219
259
  };
220
- if (config.authToken) {
221
- headers["Authorization"] = `Bearer ${config.authToken}`;
260
+ const authToken = authTokenOverride ?? config.authToken;
261
+ if (authToken) {
262
+ headers["Authorization"] = `Bearer ${authToken}`;
222
263
  }
223
264
  return headers;
224
265
  };
266
+ const fetchWithApiAuth = async (url, init, config) => {
267
+ const firstToken = await resolveAuthToken(config, false);
268
+ const firstResponse = await fetch(url, {
269
+ ...init,
270
+ headers: buildHeaders(config, firstToken),
271
+ });
272
+ if (!isSessionAuthMode(config) || firstResponse.status !== 401) {
273
+ return firstResponse;
274
+ }
275
+ const refreshedToken = await resolveAuthToken(config, true);
276
+ return fetch(url, {
277
+ ...init,
278
+ headers: buildHeaders(config, refreshedToken),
279
+ });
280
+ };
225
281
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
226
282
  // API Implementation
227
283
  const renderBuildExperimental = async (request, config) => {
@@ -233,12 +289,16 @@ const renderBuildExperimental = async (request, config) => {
233
289
  ...(request.height !== undefined ? { height: request.height } : {}),
234
290
  // Include profile if provided
235
291
  ...(request.profile ? { profile: request.profile } : {}),
292
+ ...(request.scene ? { scene: request.scene } : {}),
293
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
294
+ ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
295
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
296
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
236
297
  };
237
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
298
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
238
299
  method: "POST",
239
- headers: buildHeaders(config),
240
300
  body: JSON.stringify(requestWithFormat),
241
- });
301
+ }, config);
242
302
  if (!response.ok) {
243
303
  throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
244
304
  }
@@ -262,18 +322,23 @@ const createRenderBuildJob = async (request, config) => {
262
322
  ...(request.height !== undefined ? { height: request.height } : {}),
263
323
  // Include profile if provided
264
324
  ...(request.profile ? { profile: request.profile } : {}),
325
+ ...(request.scene ? { scene: request.scene } : {}),
326
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
265
327
  // Include composition settings
266
328
  ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
329
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
330
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
267
331
  ...(request.cameraOffsetX !== undefined ? { cameraOffsetX: request.cameraOffsetX } : {}),
268
332
  ...(request.gridSettings ? { gridSettings: request.gridSettings } : {}),
269
333
  // Include frame quality for sprite rendering
270
334
  ...(request.frameQuality ? { frameQuality: request.frameQuality } : {}),
335
+ // Include camera zoom for render-time scaling
336
+ ...(request.cameraZoom !== undefined ? { cameraZoom: request.cameraZoom } : {}),
271
337
  };
272
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
338
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
273
339
  method: "POST",
274
- headers: buildHeaders(config),
275
340
  body: JSON.stringify(body),
276
- });
341
+ }, config);
277
342
  if (!response.ok) {
278
343
  throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
279
344
  }
@@ -285,10 +350,7 @@ const createRenderBuildJob = async (request, config) => {
285
350
  };
286
351
  const getRenderBuildStatus = async (jobId, config) => {
287
352
  const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
288
- const response = await fetch(url, {
289
- method: "GET",
290
- headers: buildHeaders(config),
291
- });
353
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
292
354
  if (response.status === 404) {
293
355
  throw new Error("Render job not found");
294
356
  }
@@ -333,12 +395,16 @@ const renderSpriteExperimental = async (request, config) => {
333
395
  ...(request.height !== undefined ? { height: request.height } : {}),
334
396
  // Include profile if provided
335
397
  ...(request.profile ? { profile: request.profile } : {}),
398
+ ...(request.scene ? { scene: request.scene } : {}),
399
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
400
+ ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
401
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
402
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
336
403
  };
337
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
404
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
338
405
  method: "POST",
339
- headers: buildHeaders(config),
340
406
  body: JSON.stringify(requestWithFormat),
341
- });
407
+ }, config);
342
408
  if (!response.ok) {
343
409
  throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
344
410
  }
@@ -364,10 +430,7 @@ const getAvailableParts = async (category, config, options) => {
364
430
  params.set("skip", String(options.skip));
365
431
  const separator = base.includes("?") ? "&" : "?";
366
432
  const url = `${base}${separator}${params.toString()}`;
367
- const response = await fetch(url, {
368
- method: "GET",
369
- headers: buildHeaders(config),
370
- });
433
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
371
434
  if (!response.ok) {
372
435
  throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
373
436
  }
@@ -400,10 +463,7 @@ const getAvailableParts = async (category, config, options) => {
400
463
  */
401
464
  const getBuildByShareCode = async (shareCode, config) => {
402
465
  const url = buildApiUrl(`${API_ENDPOINTS.BUILD}/${encodeURIComponent(shareCode)}`, config);
403
- const response = await fetch(url, {
404
- method: "GET",
405
- headers: buildHeaders(config),
406
- });
466
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
407
467
  if (response.status === 404) {
408
468
  throw new Error("Build not found");
409
469
  }
@@ -433,11 +493,10 @@ const getBuildByShareCode = async (shareCode, config) => {
433
493
  */
434
494
  const getPartsByIds = async (partIds, config) => {
435
495
  const url = buildApiUrl(API_ENDPOINTS.PARTS, config);
436
- const response = await fetch(url, {
496
+ const response = await fetchWithApiAuth(url, {
437
497
  method: "POST",
438
- headers: buildHeaders(config),
439
498
  body: JSON.stringify({ ids: partIds }),
440
- });
499
+ }, config);
441
500
  if (!response.ok) {
442
501
  throw new Error(`Get parts by IDs failed: ${response.status} ${response.statusText}`);
443
502
  }
@@ -460,16 +519,20 @@ const createRenderByShareCodeJob = async (shareCode, config, options) => {
460
519
  ...(options?.width !== undefined ? { width: options.width } : {}),
461
520
  ...(options?.height !== undefined ? { height: options.height } : {}),
462
521
  ...(options?.profile ? { profile: options.profile } : {}),
522
+ ...(options?.scene ? { scene: options.scene } : {}),
523
+ ...(options?.showBackground !== undefined ? { showBackground: options.showBackground } : {}),
463
524
  ...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
525
+ ...(options?.winterMode !== undefined ? { winterMode: options.winterMode } : {}),
526
+ ...(options?.springMode !== undefined ? { springMode: options.springMode } : {}),
464
527
  ...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
465
528
  ...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
466
529
  ...(options?.frameQuality ? { frameQuality: options.frameQuality } : {}),
530
+ ...(options?.cameraZoom !== undefined ? { cameraZoom: options.cameraZoom } : {}),
467
531
  };
468
- const response = await fetch(url, {
532
+ const response = await fetchWithApiAuth(url, {
469
533
  method: "POST",
470
- headers: buildHeaders(config),
471
534
  body: JSON.stringify(body),
472
- });
535
+ }, config);
473
536
  if (response.status === 404) {
474
537
  throw new Error("Build not found");
475
538
  }
@@ -678,7 +741,21 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
678
741
  const [spriteMetadata, setSpriteMetadata] = useState(null);
679
742
  const previousInputRef = useRef(null);
680
743
  // Normalize input to SpriteRenderInput format
681
- const normalizedInput = 'type' in input ? input : { type: 'parts', parts: input };
744
+ const normalizedInput = 'type' in input
745
+ ? input
746
+ : {
747
+ type: 'parts',
748
+ parts: input,
749
+ showGrid: input.showGrid,
750
+ scene: input.scene,
751
+ showBackground: input.showBackground,
752
+ winterMode: input.winterMode,
753
+ springMode: input.springMode,
754
+ cameraOffsetX: input.cameraOffsetX,
755
+ cameraZoom: input.cameraZoom,
756
+ gridSettings: input.gridSettings,
757
+ frameQuality: input.frameQuality
758
+ };
682
759
  const fetchRenderSprite = useCallback(async (currentInput) => {
683
760
  try {
684
761
  setIsRenderingSprite(true);
@@ -690,7 +767,12 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
690
767
  format: 'sprite',
691
768
  profile: currentInput.profile,
692
769
  showGrid: currentInput.showGrid,
770
+ scene: currentInput.scene,
771
+ showBackground: currentInput.showBackground,
772
+ winterMode: currentInput.winterMode,
773
+ springMode: currentInput.springMode,
693
774
  cameraOffsetX: currentInput.cameraOffsetX,
775
+ cameraZoom: currentInput.cameraZoom,
694
776
  gridSettings: currentInput.gridSettings,
695
777
  frameQuality: currentInput.frameQuality
696
778
  });
@@ -715,7 +797,12 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
715
797
  const response = await renderSpriteExperimental({
716
798
  ...currentParts,
717
799
  showGrid: currentInput.showGrid,
800
+ scene: currentInput.scene,
801
+ showBackground: currentInput.showBackground,
802
+ winterMode: currentInput.winterMode,
803
+ springMode: currentInput.springMode,
718
804
  cameraOffsetX: currentInput.cameraOffsetX,
805
+ cameraZoom: currentInput.cameraZoom,
719
806
  gridSettings: currentInput.gridSettings,
720
807
  frameQuality,
721
808
  }, apiConfig);
@@ -740,7 +827,12 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
740
827
  ...currentParts,
741
828
  format: "sprite",
742
829
  showGrid: currentInput.showGrid,
830
+ scene: currentInput.scene,
831
+ showBackground: currentInput.showBackground,
832
+ winterMode: currentInput.winterMode,
833
+ springMode: currentInput.springMode,
743
834
  cameraOffsetX: currentInput.cameraOffsetX,
835
+ cameraZoom: currentInput.cameraZoom,
744
836
  gridSettings: currentInput.gridSettings,
745
837
  frameQuality,
746
838
  }, apiConfig);
@@ -773,7 +865,12 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
773
865
  return a.shareCode === b.shareCode &&
774
866
  a.profile === b.profile &&
775
867
  a.showGrid === b.showGrid &&
868
+ a.scene === b.scene &&
869
+ a.showBackground === b.showBackground &&
870
+ a.winterMode === b.winterMode &&
871
+ a.springMode === b.springMode &&
776
872
  a.cameraOffsetX === b.cameraOffsetX &&
873
+ a.cameraZoom === b.cameraZoom &&
777
874
  a.frameQuality === b.frameQuality &&
778
875
  gridSettingsEqual;
779
876
  }
@@ -782,7 +879,12 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
782
879
  const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
783
880
  return arePartsEqual(a.parts, b.parts) &&
784
881
  a.showGrid === b.showGrid &&
882
+ a.scene === b.scene &&
883
+ a.showBackground === b.showBackground &&
884
+ a.winterMode === b.winterMode &&
885
+ a.springMode === b.springMode &&
785
886
  a.cameraOffsetX === b.cameraOffsetX &&
887
+ a.cameraZoom === b.cameraZoom &&
786
888
  a.frameQuality === b.frameQuality &&
787
889
  gridSettingsEqual;
788
890
  }
@@ -875,10 +977,10 @@ const getTouchDistance = (touches) => {
875
977
  return 0;
876
978
  return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY);
877
979
  };
878
- const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, } = {}) => {
879
- const [scale, setScale] = useState(1);
980
+ const useZoomPan = ({ displayWidth, displayHeight, minScale = 0.5, maxScale = 2.5, initialScale = 1, } = {}) => {
981
+ const [scale, setScale] = useState(initialScale);
880
982
  const [isPinching, setIsPinching] = useState(false);
881
- const scaleRef = useRef(1);
983
+ const scaleRef = useRef(initialScale);
882
984
  const pinchDataRef = useRef({
883
985
  initialDistance: 0,
884
986
  initialScale: 1,
@@ -947,9 +1049,9 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, }
947
1049
  };
948
1050
  }, [isPinching, setScaleSafe]);
949
1051
  const reset = useCallback(() => {
950
- scaleRef.current = 1;
951
- setScale(1);
952
- }, []);
1052
+ scaleRef.current = initialScale;
1053
+ setScale(initialScale);
1054
+ }, [initialScale]);
953
1055
  return {
954
1056
  scale,
955
1057
  isPinching,
@@ -959,7 +1061,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, }
959
1061
  };
960
1062
  };
961
1063
 
962
- const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, }) => {
1064
+ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
963
1065
  const canvasRef = useRef(null);
964
1066
  const containerRef = useRef(null);
965
1067
  const [img, setImg] = useState(null);
@@ -969,26 +1071,45 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
969
1071
  const displayH = height ?? size ?? 300;
970
1072
  // Build the render input - prefer shareCode if provided (preserves interactive state like case fan slots)
971
1073
  const renderInput = useMemo(() => {
1074
+ const resolvedShowGrid = showGrid ?? parts?.showGrid;
1075
+ const resolvedScene = scene ?? parts?.scene;
1076
+ const resolvedShowBackground = showBackground ?? parts?.showBackground;
1077
+ const resolvedWinterMode = winterMode ?? parts?.winterMode;
1078
+ const resolvedSpringMode = springMode ?? parts?.springMode;
1079
+ const resolvedCameraOffsetX = cameraOffsetX ?? parts?.cameraOffsetX;
1080
+ const resolvedCameraZoom = cameraZoom ?? parts?.cameraZoom;
1081
+ const resolvedGridSettings = gridSettings ?? parts?.gridSettings;
1082
+ const resolvedFrameQuality = frameQuality ?? parts?.frameQuality;
972
1083
  if (shareCode) {
973
1084
  return {
974
1085
  type: 'shareCode',
975
1086
  shareCode,
976
1087
  profile: parts?.profile,
977
- showGrid,
978
- cameraOffsetX,
979
- gridSettings,
980
- frameQuality,
1088
+ showGrid: resolvedShowGrid,
1089
+ scene: resolvedScene,
1090
+ showBackground: resolvedShowBackground,
1091
+ winterMode: resolvedWinterMode,
1092
+ springMode: resolvedSpringMode,
1093
+ cameraOffsetX: resolvedCameraOffsetX,
1094
+ cameraZoom: resolvedCameraZoom,
1095
+ gridSettings: resolvedGridSettings,
1096
+ frameQuality: resolvedFrameQuality,
981
1097
  };
982
1098
  }
983
1099
  return {
984
1100
  type: 'parts',
985
1101
  parts: parts,
986
- showGrid,
987
- cameraOffsetX,
988
- gridSettings,
989
- frameQuality,
1102
+ showGrid: resolvedShowGrid,
1103
+ scene: resolvedScene,
1104
+ showBackground: resolvedShowBackground,
1105
+ winterMode: resolvedWinterMode,
1106
+ springMode: resolvedSpringMode,
1107
+ cameraOffsetX: resolvedCameraOffsetX,
1108
+ cameraZoom: resolvedCameraZoom,
1109
+ gridSettings: resolvedGridSettings,
1110
+ frameQuality: resolvedFrameQuality,
990
1111
  };
991
- }, [shareCode, parts, showGrid, cameraOffsetX, gridSettings, frameQuality]);
1112
+ }, [shareCode, parts, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
992
1113
  // Use custom hook for sprite rendering
993
1114
  const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
994
1115
  const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
@@ -1001,6 +1122,7 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
1001
1122
  const { scale, handleWheel: handleZoomWheel, handleTouchStart: handleZoomTouchStart, reset: resetZoom, } = useZoomPan({
1002
1123
  displayWidth: displayW,
1003
1124
  displayHeight: displayH,
1125
+ initialScale: zoom,
1004
1126
  });
1005
1127
  // Image/frame sizes - only calculate if image dimensions match expected metadata
1006
1128
  // This prevents using stale image with new metadata during transitions