@buildcores/render-client 1.0.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.
@@ -0,0 +1,582 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useState, useRef, useCallback, useEffect, createContext, useContext } from 'react';
3
+
4
+ // Helper to extract clientX from mouse or touch events
5
+ const getClientX = (e) => {
6
+ return "touches" in e ? e.touches[0].clientX : e.clientX;
7
+ };
8
+ // Helper to calculate new video time with circular wrapping
9
+ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
10
+ const timeDelta = deltaX * sensitivity;
11
+ let newTime = startTime + timeDelta;
12
+ // Make it circular - wrap around when going past boundaries
13
+ newTime = newTime % duration;
14
+ if (newTime < 0) {
15
+ newTime += duration;
16
+ }
17
+ return newTime;
18
+ };
19
+ const useVideoScrubbing = (videoRef, options = {}) => {
20
+ const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
21
+ const [isDragging, setIsDragging] = useState(false);
22
+ const [dragStartX, setDragStartX] = useState(0);
23
+ const [dragStartTime, setDragStartTime] = useState(0);
24
+ const hasDragged = useRef(false);
25
+ // Helper to start dragging (common logic for mouse and touch)
26
+ const startDrag = useCallback((clientX, event) => {
27
+ if (!videoRef.current)
28
+ return;
29
+ setIsDragging(true);
30
+ setDragStartX(clientX);
31
+ setDragStartTime(videoRef.current.currentTime);
32
+ hasDragged.current = true;
33
+ event.preventDefault();
34
+ }, [videoRef]);
35
+ // Helper to handle drag movement (common logic for mouse and touch)
36
+ const handleDragMove = useCallback((clientX, sensitivity) => {
37
+ if (!isDragging || !videoRef.current)
38
+ return;
39
+ const deltaX = clientX - dragStartX;
40
+ const duration = videoRef.current.duration || 0;
41
+ if (duration > 0) {
42
+ const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
43
+ videoRef.current.currentTime = newTime;
44
+ }
45
+ }, [isDragging, dragStartX, dragStartTime, videoRef]);
46
+ // Helper to end dragging (common logic for mouse and touch)
47
+ const endDrag = useCallback(() => {
48
+ setIsDragging(false);
49
+ }, []);
50
+ const handleMouseDown = useCallback((e) => {
51
+ startDrag(e.clientX, e.nativeEvent);
52
+ }, [startDrag]);
53
+ const handleTouchStart = useCallback((e) => {
54
+ startDrag(e.touches[0].clientX, e.nativeEvent);
55
+ }, [startDrag]);
56
+ const handleDocumentMouseMove = useCallback((e) => {
57
+ handleDragMove(getClientX(e), mouseSensitivity);
58
+ }, [handleDragMove, mouseSensitivity]);
59
+ const handleDocumentTouchMove = useCallback((e) => {
60
+ handleDragMove(getClientX(e), touchSensitivity);
61
+ }, [handleDragMove, touchSensitivity]);
62
+ const handleDocumentMouseUp = useCallback(() => {
63
+ endDrag();
64
+ }, [endDrag]);
65
+ const handleDocumentTouchEnd = useCallback(() => {
66
+ endDrag();
67
+ }, [endDrag]);
68
+ // Add document-level event listeners when dragging starts
69
+ useEffect(() => {
70
+ if (isDragging) {
71
+ document.addEventListener("mousemove", handleDocumentMouseMove);
72
+ document.addEventListener("mouseup", handleDocumentMouseUp);
73
+ document.addEventListener("touchmove", handleDocumentTouchMove);
74
+ document.addEventListener("touchend", handleDocumentTouchEnd);
75
+ return () => {
76
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
77
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
78
+ document.removeEventListener("touchmove", handleDocumentTouchMove);
79
+ document.removeEventListener("touchend", handleDocumentTouchEnd);
80
+ };
81
+ }
82
+ }, [
83
+ isDragging,
84
+ handleDocumentMouseMove,
85
+ handleDocumentMouseUp,
86
+ handleDocumentTouchMove,
87
+ handleDocumentTouchEnd,
88
+ ]);
89
+ return {
90
+ isDragging,
91
+ handleMouseDown,
92
+ handleTouchStart,
93
+ hasDragged,
94
+ };
95
+ };
96
+
97
+ /**
98
+ * @public
99
+ */
100
+ const MotionConfigContext = createContext({
101
+ transformPagePoint: (p) => p,
102
+ isStatic: false,
103
+ reducedMotion: "never",
104
+ });
105
+
106
+ /*#__NO_SIDE_EFFECTS__*/
107
+ const noop = (any) => any;
108
+
109
+ if (process.env.NODE_ENV !== "production") ;
110
+
111
+ function createRenderStep(runNextFrame) {
112
+ /**
113
+ * We create and reuse two queues, one to queue jobs for the current frame
114
+ * and one for the next. We reuse to avoid triggering GC after x frames.
115
+ */
116
+ let thisFrame = new Set();
117
+ let nextFrame = new Set();
118
+ /**
119
+ * Track whether we're currently processing jobs in this step. This way
120
+ * we can decide whether to schedule new jobs for this frame or next.
121
+ */
122
+ let isProcessing = false;
123
+ let flushNextFrame = false;
124
+ /**
125
+ * A set of processes which were marked keepAlive when scheduled.
126
+ */
127
+ const toKeepAlive = new WeakSet();
128
+ let latestFrameData = {
129
+ delta: 0.0,
130
+ timestamp: 0.0,
131
+ isProcessing: false,
132
+ };
133
+ function triggerCallback(callback) {
134
+ if (toKeepAlive.has(callback)) {
135
+ step.schedule(callback);
136
+ runNextFrame();
137
+ }
138
+ callback(latestFrameData);
139
+ }
140
+ const step = {
141
+ /**
142
+ * Schedule a process to run on the next frame.
143
+ */
144
+ schedule: (callback, keepAlive = false, immediate = false) => {
145
+ const addToCurrentFrame = immediate && isProcessing;
146
+ const queue = addToCurrentFrame ? thisFrame : nextFrame;
147
+ if (keepAlive)
148
+ toKeepAlive.add(callback);
149
+ if (!queue.has(callback))
150
+ queue.add(callback);
151
+ return callback;
152
+ },
153
+ /**
154
+ * Cancel the provided callback from running on the next frame.
155
+ */
156
+ cancel: (callback) => {
157
+ nextFrame.delete(callback);
158
+ toKeepAlive.delete(callback);
159
+ },
160
+ /**
161
+ * Execute all schedule callbacks.
162
+ */
163
+ process: (frameData) => {
164
+ latestFrameData = frameData;
165
+ /**
166
+ * If we're already processing we've probably been triggered by a flushSync
167
+ * inside an existing process. Instead of executing, mark flushNextFrame
168
+ * as true and ensure we flush the following frame at the end of this one.
169
+ */
170
+ if (isProcessing) {
171
+ flushNextFrame = true;
172
+ return;
173
+ }
174
+ isProcessing = true;
175
+ [thisFrame, nextFrame] = [nextFrame, thisFrame];
176
+ // Execute this frame
177
+ thisFrame.forEach(triggerCallback);
178
+ // Clear the frame so no callbacks remain. This is to avoid
179
+ // memory leaks should this render step not run for a while.
180
+ thisFrame.clear();
181
+ isProcessing = false;
182
+ if (flushNextFrame) {
183
+ flushNextFrame = false;
184
+ step.process(frameData);
185
+ }
186
+ },
187
+ };
188
+ return step;
189
+ }
190
+
191
+ const stepsOrder = [
192
+ "read", // Read
193
+ "resolveKeyframes", // Write/Read/Write/Read
194
+ "update", // Compute
195
+ "preRender", // Compute
196
+ "render", // Write
197
+ "postRender", // Compute
198
+ ];
199
+ const maxElapsed = 40;
200
+ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
201
+ let runNextFrame = false;
202
+ let useDefaultElapsed = true;
203
+ const state = {
204
+ delta: 0.0,
205
+ timestamp: 0.0,
206
+ isProcessing: false,
207
+ };
208
+ const flagRunNextFrame = () => (runNextFrame = true);
209
+ const steps = stepsOrder.reduce((acc, key) => {
210
+ acc[key] = createRenderStep(flagRunNextFrame);
211
+ return acc;
212
+ }, {});
213
+ const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
214
+ const processBatch = () => {
215
+ const timestamp = performance.now();
216
+ runNextFrame = false;
217
+ state.delta = useDefaultElapsed
218
+ ? 1000 / 60
219
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
220
+ state.timestamp = timestamp;
221
+ state.isProcessing = true;
222
+ // Unrolled render loop for better per-frame performance
223
+ read.process(state);
224
+ resolveKeyframes.process(state);
225
+ update.process(state);
226
+ preRender.process(state);
227
+ render.process(state);
228
+ postRender.process(state);
229
+ state.isProcessing = false;
230
+ if (runNextFrame && allowKeepAlive) {
231
+ useDefaultElapsed = false;
232
+ scheduleNextBatch(processBatch);
233
+ }
234
+ };
235
+ const wake = () => {
236
+ runNextFrame = true;
237
+ useDefaultElapsed = true;
238
+ if (!state.isProcessing) {
239
+ scheduleNextBatch(processBatch);
240
+ }
241
+ };
242
+ const schedule = stepsOrder.reduce((acc, key) => {
243
+ const step = steps[key];
244
+ acc[key] = (process, keepAlive = false, immediate = false) => {
245
+ if (!runNextFrame)
246
+ wake();
247
+ return step.schedule(process, keepAlive, immediate);
248
+ };
249
+ return acc;
250
+ }, {});
251
+ const cancel = (process) => {
252
+ for (let i = 0; i < stepsOrder.length; i++) {
253
+ steps[stepsOrder[i]].cancel(process);
254
+ }
255
+ };
256
+ return { schedule, cancel, state, steps };
257
+ }
258
+
259
+ const { schedule: frame, cancel: cancelFrame} = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
260
+
261
+ function useAnimationFrame(callback) {
262
+ const initialTimestamp = useRef(0);
263
+ const { isStatic } = useContext(MotionConfigContext);
264
+ useEffect(() => {
265
+ if (isStatic)
266
+ return;
267
+ const provideTimeSinceStart = ({ timestamp, delta }) => {
268
+ if (!initialTimestamp.current)
269
+ initialTimestamp.current = timestamp;
270
+ callback(timestamp - initialTimestamp.current, delta);
271
+ };
272
+ frame.update(provideTimeSinceStart, true);
273
+ return () => cancelFrame(provideTimeSinceStart);
274
+ }, [callback]);
275
+ }
276
+
277
+ function useBouncePatternProgress(enabled = true) {
278
+ const [value, setValue] = useState(0);
279
+ const [isBouncing, setIsBouncing] = useState(false);
280
+ const start = useRef(null);
281
+ useAnimationFrame((t) => {
282
+ if (!enabled) {
283
+ // Reset animation when disabled
284
+ if (start.current !== null) {
285
+ start.current = null;
286
+ setValue(0);
287
+ setIsBouncing(false);
288
+ }
289
+ return;
290
+ }
291
+ if (start.current === null)
292
+ start.current = t;
293
+ const elapsed = (t - start.current) % 3000; // 3s full cycle
294
+ let progress = 0;
295
+ const bouncing = elapsed < 1000; // Bouncing during first 1 second
296
+ if (elapsed < 500) {
297
+ // 0 → 1
298
+ progress = elapsed / 500;
299
+ }
300
+ else if (elapsed < 1000) {
301
+ // 1 → 0
302
+ progress = 1 - (elapsed - 500) / 500;
303
+ }
304
+ else {
305
+ // Pause at 0 for 2 seconds
306
+ progress = 0;
307
+ }
308
+ setValue(progress);
309
+ setIsBouncing(bouncing);
310
+ });
311
+ return { value, isBouncing };
312
+ }
313
+
314
+ // API Types
315
+ /**
316
+ * Enum defining all available PC part categories that can be rendered.
317
+ *
318
+ * Each category represents a different type of computer component that can be
319
+ * included in the 3D build visualization.
320
+ *
321
+ * @example
322
+ * ```tsx
323
+ * // All available categories
324
+ * const categories = [
325
+ * PartCategory.CPU, // "CPU"
326
+ * PartCategory.GPU, // "GPU"
327
+ * PartCategory.RAM, // "RAM"
328
+ * PartCategory.Motherboard,// "Motherboard"
329
+ * PartCategory.PSU, // "PSU"
330
+ * PartCategory.Storage, // "Storage"
331
+ * PartCategory.PCCase, // "PCCase"
332
+ * PartCategory.CPUCooler, // "CPUCooler"
333
+ * ];
334
+ * ```
335
+ */
336
+ var PartCategory;
337
+ (function (PartCategory) {
338
+ /** Central Processing Unit - The main processor */
339
+ PartCategory["CPU"] = "CPU";
340
+ /** Graphics Processing Unit - Video card for rendering */
341
+ PartCategory["GPU"] = "GPU";
342
+ /** Random Access Memory - System memory modules */
343
+ PartCategory["RAM"] = "RAM";
344
+ /** Main circuit board that connects all components */
345
+ PartCategory["Motherboard"] = "Motherboard";
346
+ /** Power Supply Unit - Provides power to all components */
347
+ PartCategory["PSU"] = "PSU";
348
+ /** Storage devices like SSDs, HDDs, NVMe drives */
349
+ PartCategory["Storage"] = "Storage";
350
+ /** PC Case - The enclosure that houses all components */
351
+ PartCategory["PCCase"] = "PCCase";
352
+ /** CPU Cooler - Air or liquid cooling for the processor */
353
+ PartCategory["CPUCooler"] = "CPUCooler";
354
+ })(PartCategory || (PartCategory = {}));
355
+
356
+ // API Configuration
357
+ const API_BASE_URL = "https://squid-app-7aeyk.ondigitalocean.app";
358
+ // API Endpoints
359
+ const API_ENDPOINTS = {
360
+ RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental",
361
+ AVAILABLE_PARTS: "/available-parts",
362
+ };
363
+ // API URL helpers
364
+ const buildApiUrl = (endpoint) => {
365
+ return `${API_BASE_URL}${endpoint}`;
366
+ };
367
+ // API Implementation
368
+ const renderBuildExperimental = async (request) => {
369
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
370
+ method: "POST",
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ },
374
+ body: JSON.stringify(request),
375
+ });
376
+ if (!response.ok) {
377
+ throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
378
+ }
379
+ const video = await response.blob();
380
+ return {
381
+ video,
382
+ metadata: {
383
+ size: video.size,
384
+ format: "video/mp4",
385
+ },
386
+ };
387
+ };
388
+ const getAvailableParts = async () => {
389
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.AVAILABLE_PARTS), {
390
+ method: "GET",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ },
394
+ });
395
+ if (!response.ok) {
396
+ throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
397
+ }
398
+ return response.json();
399
+ };
400
+
401
+ /**
402
+ * Compares two RenderBuildRequest objects for equality by checking if the same IDs
403
+ * are present in each category array, regardless of order.
404
+ */
405
+ const arePartsEqual = (parts1, parts2) => {
406
+ const categories = Object.values(PartCategory);
407
+ for (const category of categories) {
408
+ const arr1 = parts1.parts[category] || [];
409
+ const arr2 = parts2.parts[category] || [];
410
+ // Check if arrays have the same length
411
+ if (arr1.length !== arr2.length) {
412
+ return false;
413
+ }
414
+ // Check if arrays contain the same elements (order doesn't matter)
415
+ const set1 = new Set(arr1);
416
+ const set2 = new Set(arr2);
417
+ if (set1.size !== set2.size) {
418
+ return false;
419
+ }
420
+ for (const id of set1) {
421
+ if (!set2.has(id)) {
422
+ return false;
423
+ }
424
+ }
425
+ }
426
+ return true;
427
+ };
428
+ const useBuildRender = (parts, onLoadStart) => {
429
+ const [videoSrc, setVideoSrc] = useState(null);
430
+ const [isRenderingBuild, setIsRenderingBuild] = useState(false);
431
+ const [renderError, setRenderError] = useState(null);
432
+ const previousPartsRef = useRef(null);
433
+ const fetchRenderBuild = useCallback(async (currentParts) => {
434
+ try {
435
+ setIsRenderingBuild(true);
436
+ setRenderError(null);
437
+ onLoadStart?.();
438
+ const response = await renderBuildExperimental(currentParts);
439
+ const objectUrl = URL.createObjectURL(response.video);
440
+ // Clean up previous video URL before setting new one
441
+ setVideoSrc((prevSrc) => {
442
+ if (prevSrc) {
443
+ URL.revokeObjectURL(prevSrc);
444
+ }
445
+ return objectUrl;
446
+ });
447
+ }
448
+ catch (error) {
449
+ setRenderError(error instanceof Error ? error.message : "Failed to render build");
450
+ }
451
+ finally {
452
+ setIsRenderingBuild(false);
453
+ }
454
+ }, [onLoadStart]);
455
+ // Effect to call API when parts content changes (using custom equality check)
456
+ useEffect(() => {
457
+ const shouldFetch = previousPartsRef.current === null ||
458
+ !arePartsEqual(previousPartsRef.current, parts);
459
+ if (shouldFetch) {
460
+ previousPartsRef.current = parts;
461
+ fetchRenderBuild(parts);
462
+ }
463
+ }, [parts, fetchRenderBuild]);
464
+ // Cleanup effect for component unmount
465
+ useEffect(() => {
466
+ return () => {
467
+ if (videoSrc) {
468
+ URL.revokeObjectURL(videoSrc);
469
+ }
470
+ };
471
+ }, [videoSrc]);
472
+ return {
473
+ videoSrc,
474
+ isRenderingBuild,
475
+ renderError,
476
+ };
477
+ };
478
+
479
+ const LoadingErrorOverlay = ({ isVisible, renderError, size, }) => {
480
+ if (!isVisible)
481
+ return null;
482
+ return (jsx("div", { style: {
483
+ position: "absolute",
484
+ top: 0,
485
+ left: 0,
486
+ right: 0,
487
+ bottom: 0,
488
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
489
+ display: "flex",
490
+ flexDirection: "column",
491
+ alignItems: "center",
492
+ justifyContent: "center",
493
+ color: "white",
494
+ zIndex: 10,
495
+ }, children: renderError ? (jsxs(Fragment, { children: [jsx("div", { style: { marginBottom: "20px", fontSize: "18px" }, children: "Render Failed" }), jsx("div", { style: {
496
+ fontSize: "14px",
497
+ textAlign: "center",
498
+ maxWidth: size * 0.8,
499
+ }, children: renderError })] })) : (jsx(Fragment, { children: jsx("div", { style: { marginBottom: "20px", fontSize: "18px" }, children: "Loading Build..." }) })) }));
500
+ };
501
+
502
+ const DragIcon = ({ width = 24, height = 24, className, style, ...props }) => {
503
+ return (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", id: "Layer_1", width: width, height: height, "data-name": "Layer 1", viewBox: "0 0 24 24", className: className, style: style, ...props, children: [jsx("defs", { children: jsx("style", { children: ".cls-1{fill:none;stroke:currentColor;stroke-miterlimit:10;stroke-width:1.91px}" }) }), jsx("path", { d: "m11.05 22.5-5.14-5.14a2 2 0 0 1-.59-1.43 2 2 0 0 1 2-2 2 2 0 0 1 1.43.59l1.32 1.32V6.38a2 2 0 0 1 1.74-2 1.89 1.89 0 0 1 1.52.56 1.87 1.87 0 0 1 .56 1.34V12l5 .72a1.91 1.91 0 0 1 1.64 1.89 17.18 17.18 0 0 1-1.82 7.71l-.09.18M19.64 7.23l2.86-2.87-2.86-2.86M15.82 4.36h6.68M4.36 7.23 1.5 4.36 4.36 1.5M8.18 4.36H1.5", className: "cls-1" })] }));
504
+ };
505
+
506
+ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
507
+ if (!isVisible) {
508
+ return null;
509
+ }
510
+ return (jsx("div", { style: {
511
+ position: "absolute",
512
+ top: "50%",
513
+ left: "50%",
514
+ transform: `translate(-50%, -50%) translateX(${progressValue * 100}px)`,
515
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
516
+ color: "white",
517
+ padding: "12px",
518
+ borderRadius: "8px",
519
+ pointerEvents: "none",
520
+ zIndex: 5,
521
+ display: "flex",
522
+ alignItems: "center",
523
+ justifyContent: "center",
524
+ }, children: instructionIcon ? (jsx("img", { src: instructionIcon, alt: "drag to view 360", style: {
525
+ width: "24px",
526
+ height: "24px",
527
+ filter: "invert(1)", // Makes the icon white
528
+ } })) : (jsx(DragIcon, { width: 24, height: 24, style: {
529
+ color: "white",
530
+ } })) }));
531
+ };
532
+
533
+ const BuildRender = ({ parts, size, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
534
+ const videoRef = useRef(null);
535
+ const [isLoading, setIsLoading] = useState(true);
536
+ const [bouncingAllowed, setBouncingAllowed] = useState(false);
537
+ // Use custom hook for build rendering
538
+ const { videoSrc, isRenderingBuild, renderError } = useBuildRender(parts);
539
+ const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
540
+ const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useVideoScrubbing(videoRef, {
541
+ mouseSensitivity,
542
+ touchSensitivity,
543
+ });
544
+ const handleLoadStartInternal = useCallback(() => {
545
+ setIsLoading(true);
546
+ setBouncingAllowed(false);
547
+ }, []);
548
+ const handleCanPlayInternal = useCallback(() => {
549
+ setIsLoading(false);
550
+ // Start bouncing animation after delay
551
+ setTimeout(() => {
552
+ setBouncingAllowed(true);
553
+ }, 2000);
554
+ }, []);
555
+ useEffect(() => {
556
+ if (hasDragged.current || !videoRef.current)
557
+ return;
558
+ const duration = videoRef.current.duration;
559
+ if (!isFinite(duration))
560
+ return;
561
+ const time = calculateCircularTime(0, progressValue, 0.5, duration);
562
+ if (isFinite(time)) {
563
+ videoRef.current.currentTime = time;
564
+ }
565
+ }, [progressValue, hasDragged]);
566
+ return (jsxs("div", { style: { position: "relative", width: size, height: size }, children: [videoSrc && (jsx("video", { ref: videoRef, src: videoSrc, width: size, height: size, autoPlay: true, preload: "metadata", muted: true, playsInline: true, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onLoadStart: handleLoadStartInternal, onCanPlay: handleCanPlayInternal, onLoadedData: () => {
567
+ if (videoRef.current) {
568
+ videoRef.current.pause();
569
+ }
570
+ }, style: {
571
+ cursor: isDragging ? "grabbing" : "grab",
572
+ touchAction: "none", // Prevents default touch behaviors like scrolling
573
+ display: "block",
574
+ }, children: "Your browser does not support the video tag." }, videoSrc)), jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingBuild || !!renderError, renderError: renderError || undefined, size: size }), jsx(InstructionTooltip, { isVisible: !isLoading &&
575
+ !isRenderingBuild &&
576
+ !renderError &&
577
+ isBouncing &&
578
+ !hasDragged.current, progressValue: progressValue })] }));
579
+ };
580
+
581
+ export { API_BASE_URL, API_ENDPOINTS, BuildRender, DragIcon, InstructionTooltip, LoadingErrorOverlay, PartCategory, arePartsEqual, buildApiUrl, calculateCircularTime, getAvailableParts, renderBuildExperimental, useBouncePatternProgress, useBuildRender, useVideoScrubbing };
582
+ //# sourceMappingURL=index.esm.js.map