@buildcores/render-client 1.6.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/README.md CHANGED
@@ -45,6 +45,33 @@ function App() {
45
45
  }
46
46
  ```
47
47
 
48
+ ## 🔐 Authentication Modes
49
+
50
+ `@buildcores/render-client` supports two auth modes:
51
+
52
+ - `legacy` (backward-compatible): sends a long-lived API key from `apiConfig.authToken`.
53
+ - `session` (recommended): fetches short-lived delegated tokens from your backend via `apiConfig.getRenderSessionToken`.
54
+
55
+ ```tsx
56
+ const apiConfig = {
57
+ environment: "prod",
58
+ authMode: "session",
59
+ getRenderSessionToken: async () => {
60
+ // Your backend endpoint should broker BuildCores /auth/render-session
61
+ const res = await fetch("/buildcores/session", { method: "POST" });
62
+ if (!res.ok) throw new Error("Failed to mint BuildCores session");
63
+ const data = await res.json();
64
+ return {
65
+ token: data.session_token,
66
+ expiresAt: data.expires_at,
67
+ };
68
+ },
69
+ };
70
+ ```
71
+
72
+ Legacy browser API key flow is deprecated and should only be used for temporary compatibility.
73
+ `/render-build-experimental` behavior is unchanged.
74
+
48
75
  ## 🔧 API Reference
49
76
 
50
77
  ### `BuildRender` Component
@@ -72,6 +99,11 @@ interface RenderBuildRequest {
72
99
  format?: "video" | "sprite";
73
100
  width?: number; // Optional: Canvas pixel width (256-2000)
74
101
  height?: number; // Optional: Canvas pixel height (256-2000)
102
+ scene?: "sunset" | "dawn" | "night" | "warehouse" | "forest" | "apartment" | "studio" | "studio_v2" | "city" | "park" | "lobby";
103
+ showBackground?: boolean;
104
+ showGrid?: boolean;
105
+ winterMode?: boolean; // mutually exclusive with springMode
106
+ springMode?: boolean; // mutually exclusive with winterMode
75
107
  }
76
108
 
77
109
  // Available part categories
@@ -91,6 +123,8 @@ enum PartCategory {
91
123
 
92
124
  **Resolution Control**: You can specify custom `width` and `height` (both must be provided together, 256-2000 pixels) for higher or lower quality renders. If not specified, the default resolution is used.
93
125
 
126
+ **Environment Controls**: You can configure `scene`, `showBackground`, `showGrid`, and seasonal toggles (`winterMode` / `springMode`) for async render endpoints.
127
+
94
128
  #### Examples
95
129
 
96
130
  **Complete Build (All Components)**
package/dist/api.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface RenderJobStatusResponse {
32
32
  url?: string | null;
33
33
  video_url?: string | null;
34
34
  sprite_url?: string | null;
35
+ screenshot_url?: string | null;
35
36
  error?: string | null;
36
37
  end_time?: string | null;
37
38
  }
@@ -71,7 +72,7 @@ export interface RenderAPIService {
71
72
  getAvailableParts(category: PartCategory, config: ApiConfig, options?: GetAvailablePartsOptions): Promise<AvailablePartsResponse>;
72
73
  }
73
74
  export declare const buildApiUrl: (endpoint: string, config: ApiConfig) => string;
74
- export declare const buildHeaders: (config: ApiConfig) => Record<string, string>;
75
+ export declare const buildHeaders: (config: ApiConfig, authTokenOverride?: string) => Record<string, string>;
75
76
  export declare const renderBuildExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderBuildResponse>;
76
77
  export declare const createRenderBuildJob: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderJobCreateResponse>;
77
78
  export declare const getRenderBuildStatus: (jobId: string, config: ApiConfig) => Promise<RenderJobStatusResponse>;
@@ -34,6 +34,10 @@ export type SpriteRenderInput = {
34
34
  type: 'parts';
35
35
  parts: RenderBuildRequest;
36
36
  showGrid?: boolean;
37
+ scene?: RenderBuildRequest["scene"];
38
+ showBackground?: boolean;
39
+ winterMode?: boolean;
40
+ springMode?: boolean;
37
41
  cameraOffsetX?: number;
38
42
  cameraZoom?: number;
39
43
  gridSettings?: RenderGridSettings;
@@ -43,6 +47,10 @@ export type SpriteRenderInput = {
43
47
  shareCode: string;
44
48
  profile?: 'cinematic' | 'flat' | 'fast';
45
49
  showGrid?: boolean;
50
+ scene?: RenderBuildRequest["scene"];
51
+ showBackground?: boolean;
52
+ winterMode?: boolean;
53
+ springMode?: boolean;
46
54
  cameraOffsetX?: number;
47
55
  cameraZoom?: number;
48
56
  gridSettings?: RenderGridSettings;
package/dist/index.d.ts CHANGED
@@ -229,6 +229,24 @@ interface BuildRenderProps {
229
229
  * Works for both parts and shareCode rendering.
230
230
  */
231
231
  showGrid?: boolean;
232
+ /**
233
+ * Environment scene preset for rendering.
234
+ */
235
+ scene?: RenderScene;
236
+ /**
237
+ * Whether to show the environment background.
238
+ */
239
+ showBackground?: boolean;
240
+ /**
241
+ * Enable winter mode effects.
242
+ * Mutually exclusive with springMode.
243
+ */
244
+ winterMode?: boolean;
245
+ /**
246
+ * Enable spring mode effects.
247
+ * Mutually exclusive with winterMode.
248
+ */
249
+ springMode?: boolean;
232
250
  /**
233
251
  * Camera offset X for composition.
234
252
  * Positive values shift the build to the right, leaving room for text overlay on the left.
@@ -386,6 +404,22 @@ interface ApiConfig {
386
404
  * ```
387
405
  */
388
406
  authToken?: string;
407
+ /**
408
+ * Auth mode for API requests.
409
+ * - legacy: Sends `authToken` directly as bearer token.
410
+ * - session: Uses `getRenderSessionToken` to fetch short-lived delegated tokens.
411
+ *
412
+ * @default "legacy"
413
+ */
414
+ authMode?: 'legacy' | 'session';
415
+ /**
416
+ * Session token supplier used when `authMode` is "session".
417
+ * Should call your backend endpoint that brokers BuildCores session tokens.
418
+ */
419
+ getRenderSessionToken?: () => Promise<{
420
+ token: string;
421
+ expiresAt: string;
422
+ }>;
389
423
  }
390
424
  /**
391
425
  * Enum defining all available PC part categories that can be rendered.
@@ -552,6 +586,24 @@ interface RenderBuildRequest {
552
586
  * Defaults to true for cinematic profile, false otherwise.
553
587
  */
554
588
  showGrid?: boolean;
589
+ /**
590
+ * Environment scene preset.
591
+ */
592
+ scene?: RenderScene;
593
+ /**
594
+ * Whether to show the environment background.
595
+ */
596
+ showBackground?: boolean;
597
+ /**
598
+ * Enable winter mode effects.
599
+ * Mutually exclusive with springMode.
600
+ */
601
+ winterMode?: boolean;
602
+ /**
603
+ * Enable spring mode effects.
604
+ * Mutually exclusive with winterMode.
605
+ */
606
+ springMode?: boolean;
555
607
  /**
556
608
  * Horizontal offset for the camera view.
557
609
  * Positive values shift the build to the right, leaving room for text overlay on the left.
@@ -759,6 +811,10 @@ interface GridSettings {
759
811
  /** Render order for depth sorting (default: 0, use -1 to render before other objects) */
760
812
  renderOrder?: number;
761
813
  }
814
+ /**
815
+ * Supported environment scene presets for render API endpoints.
816
+ */
817
+ type RenderScene = "sunset" | "dawn" | "night" | "warehouse" | "forest" | "apartment" | "studio" | "studio_v2" | "city" | "park" | "lobby";
762
818
  /**
763
819
  * Options for rendering a build by share code
764
820
  */
@@ -771,8 +827,16 @@ interface RenderByShareCodeOptions {
771
827
  height?: number;
772
828
  /** Render quality profile */
773
829
  profile?: "cinematic" | "flat" | "fast";
830
+ /** Environment scene preset */
831
+ scene?: RenderScene;
832
+ /** Whether to show the environment background */
833
+ showBackground?: boolean;
774
834
  /** Show grid in render (default: true for cinematic profile) */
775
835
  showGrid?: boolean;
836
+ /** Enable winter mode effects (mutually exclusive with springMode) */
837
+ winterMode?: boolean;
838
+ /** Enable spring mode effects (mutually exclusive with winterMode) */
839
+ springMode?: boolean;
776
840
  /** Camera offset X for composition (positive = shift build right to leave room for text overlay) */
777
841
  cameraOffsetX?: number;
778
842
  /** Grid appearance settings (for thicker/more visible grid in renders) */
@@ -922,6 +986,10 @@ type SpriteRenderInput = {
922
986
  type: 'parts';
923
987
  parts: RenderBuildRequest;
924
988
  showGrid?: boolean;
989
+ scene?: RenderBuildRequest["scene"];
990
+ showBackground?: boolean;
991
+ winterMode?: boolean;
992
+ springMode?: boolean;
925
993
  cameraOffsetX?: number;
926
994
  cameraZoom?: number;
927
995
  gridSettings?: RenderGridSettings;
@@ -931,6 +999,10 @@ type SpriteRenderInput = {
931
999
  shareCode: string;
932
1000
  profile?: 'cinematic' | 'flat' | 'fast';
933
1001
  showGrid?: boolean;
1002
+ scene?: RenderBuildRequest["scene"];
1003
+ showBackground?: boolean;
1004
+ winterMode?: boolean;
1005
+ springMode?: boolean;
934
1006
  cameraOffsetX?: number;
935
1007
  cameraZoom?: number;
936
1008
  gridSettings?: RenderGridSettings;
@@ -983,6 +1055,20 @@ interface RenderBuildResponse {
983
1055
  format?: string;
984
1056
  };
985
1057
  }
1058
+ interface RenderJobCreateResponse {
1059
+ job_id: string;
1060
+ status: "queued" | "processing" | "completed" | "error";
1061
+ }
1062
+ interface RenderJobStatusResponse {
1063
+ job_id: string;
1064
+ status: "queued" | "processing" | "completed" | "error";
1065
+ url?: string | null;
1066
+ video_url?: string | null;
1067
+ sprite_url?: string | null;
1068
+ screenshot_url?: string | null;
1069
+ error?: string | null;
1070
+ end_time?: string | null;
1071
+ }
986
1072
  interface RenderSpriteResponse {
987
1073
  /**
988
1074
  * The rendered sprite sheet as a Blob (when format is "sprite")
@@ -1015,7 +1101,7 @@ interface RenderAPIService {
1015
1101
  getAvailableParts(category: PartCategory, config: ApiConfig, options?: GetAvailablePartsOptions): Promise<AvailablePartsResponse>;
1016
1102
  }
1017
1103
  declare const buildApiUrl: (endpoint: string, config: ApiConfig) => string;
1018
- declare const buildHeaders: (config: ApiConfig) => Record<string, string>;
1104
+ declare const buildHeaders: (config: ApiConfig, authTokenOverride?: string) => Record<string, string>;
1019
1105
  declare const renderBuildExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderBuildResponse>;
1020
1106
  declare const renderSpriteExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderSpriteResponse>;
1021
1107
  declare const getAvailableParts: (category: PartCategory, config: ApiConfig, options?: GetAvailablePartsOptions) => Promise<AvailablePartsResponse>;
@@ -1103,4 +1189,4 @@ declare const createRenderByShareCodeJob: (shareCode: string, config: ApiConfig,
1103
1189
  declare const renderByShareCode: (shareCode: string, config: ApiConfig, options?: RenderByShareCodeOptions) => Promise<RenderByShareCodeResponse>;
1104
1190
 
1105
1191
  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, useContinuousSpin, useSpriteRender, useSpriteScrubbing, useVideoScrubbing };
1106
- export type { ApiConfig, AvailablePartsResponse, BuildRenderProps, BuildRenderVideoProps, BuildResponse, GetAvailablePartsOptions, GridSettings, PartDetails, PartDetailsWithCategory, PartsResponse, RenderAPIService, RenderBuildRequest, RenderBuildResponse, RenderByShareCodeJobResponse, RenderByShareCodeOptions, RenderByShareCodeResponse, RenderSpriteResponse, SpriteRenderInput, UseBuildRenderOptions, UseBuildRenderReturn, UseSpriteRenderOptions, UseSpriteRenderReturn };
1192
+ export type { ApiConfig, AvailablePartsResponse, BuildRenderProps, BuildRenderVideoProps, BuildResponse, GetAvailablePartsOptions, GridSettings, PartDetails, PartDetailsWithCategory, PartsResponse, RenderAPIService, RenderBuildRequest, RenderBuildResponse, RenderByShareCodeJobResponse, RenderByShareCodeOptions, RenderByShareCodeResponse, RenderJobCreateResponse, RenderJobStatusResponse, RenderScene, RenderSpriteResponse, SpriteRenderInput, UseBuildRenderOptions, UseBuildRenderReturn, UseSpriteRenderOptions, UseSpriteRenderReturn };
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,8 +322,12 @@ 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
@@ -271,11 +335,10 @@ const createRenderBuildJob = async (request, config) => {
271
335
  // Include camera zoom for render-time scaling
272
336
  ...(request.cameraZoom !== undefined ? { cameraZoom: request.cameraZoom } : {}),
273
337
  };
274
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
338
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
275
339
  method: "POST",
276
- headers: buildHeaders(config),
277
340
  body: JSON.stringify(body),
278
- });
341
+ }, config);
279
342
  if (!response.ok) {
280
343
  throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
281
344
  }
@@ -287,10 +350,7 @@ const createRenderBuildJob = async (request, config) => {
287
350
  };
288
351
  const getRenderBuildStatus = async (jobId, config) => {
289
352
  const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
290
- const response = await fetch(url, {
291
- method: "GET",
292
- headers: buildHeaders(config),
293
- });
353
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
294
354
  if (response.status === 404) {
295
355
  throw new Error("Render job not found");
296
356
  }
@@ -335,12 +395,16 @@ const renderSpriteExperimental = async (request, config) => {
335
395
  ...(request.height !== undefined ? { height: request.height } : {}),
336
396
  // Include profile if provided
337
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 } : {}),
338
403
  };
339
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
404
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
340
405
  method: "POST",
341
- headers: buildHeaders(config),
342
406
  body: JSON.stringify(requestWithFormat),
343
- });
407
+ }, config);
344
408
  if (!response.ok) {
345
409
  throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
346
410
  }
@@ -366,10 +430,7 @@ const getAvailableParts = async (category, config, options) => {
366
430
  params.set("skip", String(options.skip));
367
431
  const separator = base.includes("?") ? "&" : "?";
368
432
  const url = `${base}${separator}${params.toString()}`;
369
- const response = await fetch(url, {
370
- method: "GET",
371
- headers: buildHeaders(config),
372
- });
433
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
373
434
  if (!response.ok) {
374
435
  throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
375
436
  }
@@ -402,10 +463,7 @@ const getAvailableParts = async (category, config, options) => {
402
463
  */
403
464
  const getBuildByShareCode = async (shareCode, config) => {
404
465
  const url = buildApiUrl(`${API_ENDPOINTS.BUILD}/${encodeURIComponent(shareCode)}`, config);
405
- const response = await fetch(url, {
406
- method: "GET",
407
- headers: buildHeaders(config),
408
- });
466
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
409
467
  if (response.status === 404) {
410
468
  throw new Error("Build not found");
411
469
  }
@@ -435,11 +493,10 @@ const getBuildByShareCode = async (shareCode, config) => {
435
493
  */
436
494
  const getPartsByIds = async (partIds, config) => {
437
495
  const url = buildApiUrl(API_ENDPOINTS.PARTS, config);
438
- const response = await fetch(url, {
496
+ const response = await fetchWithApiAuth(url, {
439
497
  method: "POST",
440
- headers: buildHeaders(config),
441
498
  body: JSON.stringify({ ids: partIds }),
442
- });
499
+ }, config);
443
500
  if (!response.ok) {
444
501
  throw new Error(`Get parts by IDs failed: ${response.status} ${response.statusText}`);
445
502
  }
@@ -462,17 +519,20 @@ const createRenderByShareCodeJob = async (shareCode, config, options) => {
462
519
  ...(options?.width !== undefined ? { width: options.width } : {}),
463
520
  ...(options?.height !== undefined ? { height: options.height } : {}),
464
521
  ...(options?.profile ? { profile: options.profile } : {}),
522
+ ...(options?.scene ? { scene: options.scene } : {}),
523
+ ...(options?.showBackground !== undefined ? { showBackground: options.showBackground } : {}),
465
524
  ...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
525
+ ...(options?.winterMode !== undefined ? { winterMode: options.winterMode } : {}),
526
+ ...(options?.springMode !== undefined ? { springMode: options.springMode } : {}),
466
527
  ...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
467
528
  ...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
468
529
  ...(options?.frameQuality ? { frameQuality: options.frameQuality } : {}),
469
530
  ...(options?.cameraZoom !== undefined ? { cameraZoom: options.cameraZoom } : {}),
470
531
  };
471
- const response = await fetch(url, {
532
+ const response = await fetchWithApiAuth(url, {
472
533
  method: "POST",
473
- headers: buildHeaders(config),
474
534
  body: JSON.stringify(body),
475
- });
535
+ }, config);
476
536
  if (response.status === 404) {
477
537
  throw new Error("Build not found");
478
538
  }
@@ -681,7 +741,21 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
681
741
  const [spriteMetadata, setSpriteMetadata] = useState(null);
682
742
  const previousInputRef = useRef(null);
683
743
  // Normalize input to SpriteRenderInput format
684
- 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
+ };
685
759
  const fetchRenderSprite = useCallback(async (currentInput) => {
686
760
  try {
687
761
  setIsRenderingSprite(true);
@@ -693,6 +767,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
693
767
  format: 'sprite',
694
768
  profile: currentInput.profile,
695
769
  showGrid: currentInput.showGrid,
770
+ scene: currentInput.scene,
771
+ showBackground: currentInput.showBackground,
772
+ winterMode: currentInput.winterMode,
773
+ springMode: currentInput.springMode,
696
774
  cameraOffsetX: currentInput.cameraOffsetX,
697
775
  cameraZoom: currentInput.cameraZoom,
698
776
  gridSettings: currentInput.gridSettings,
@@ -719,6 +797,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
719
797
  const response = await renderSpriteExperimental({
720
798
  ...currentParts,
721
799
  showGrid: currentInput.showGrid,
800
+ scene: currentInput.scene,
801
+ showBackground: currentInput.showBackground,
802
+ winterMode: currentInput.winterMode,
803
+ springMode: currentInput.springMode,
722
804
  cameraOffsetX: currentInput.cameraOffsetX,
723
805
  cameraZoom: currentInput.cameraZoom,
724
806
  gridSettings: currentInput.gridSettings,
@@ -745,6 +827,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
745
827
  ...currentParts,
746
828
  format: "sprite",
747
829
  showGrid: currentInput.showGrid,
830
+ scene: currentInput.scene,
831
+ showBackground: currentInput.showBackground,
832
+ winterMode: currentInput.winterMode,
833
+ springMode: currentInput.springMode,
748
834
  cameraOffsetX: currentInput.cameraOffsetX,
749
835
  cameraZoom: currentInput.cameraZoom,
750
836
  gridSettings: currentInput.gridSettings,
@@ -779,6 +865,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
779
865
  return a.shareCode === b.shareCode &&
780
866
  a.profile === b.profile &&
781
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 &&
782
872
  a.cameraOffsetX === b.cameraOffsetX &&
783
873
  a.cameraZoom === b.cameraZoom &&
784
874
  a.frameQuality === b.frameQuality &&
@@ -789,6 +879,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
789
879
  const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
790
880
  return arePartsEqual(a.parts, b.parts) &&
791
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 &&
792
886
  a.cameraOffsetX === b.cameraOffsetX &&
793
887
  a.cameraZoom === b.cameraZoom &&
794
888
  a.frameQuality === b.frameQuality &&
@@ -967,7 +1061,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 0.5, maxScale = 2.
967
1061
  };
968
1062
  };
969
1063
 
970
- const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
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, }) => {
971
1065
  const canvasRef = useRef(null);
972
1066
  const containerRef = useRef(null);
973
1067
  const [img, setImg] = useState(null);
@@ -977,28 +1071,45 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
977
1071
  const displayH = height ?? size ?? 300;
978
1072
  // Build the render input - prefer shareCode if provided (preserves interactive state like case fan slots)
979
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;
980
1083
  if (shareCode) {
981
1084
  return {
982
1085
  type: 'shareCode',
983
1086
  shareCode,
984
1087
  profile: parts?.profile,
985
- showGrid,
986
- cameraOffsetX,
987
- cameraZoom,
988
- gridSettings,
989
- 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,
990
1097
  };
991
1098
  }
992
1099
  return {
993
1100
  type: 'parts',
994
1101
  parts: parts,
995
- showGrid,
996
- cameraOffsetX,
997
- cameraZoom,
998
- gridSettings,
999
- 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,
1000
1111
  };
1001
- }, [shareCode, parts, showGrid, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
1112
+ }, [shareCode, parts, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
1002
1113
  // Use custom hook for sprite rendering
1003
1114
  const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
1004
1115
  const total = spriteMetadata ? spriteMetadata.totalFrames : 72;