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