From 9c1e1b37b16587fddef9eebab0215e83d476a268 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Mon, 20 Jul 2026 01:55:15 +0300 Subject: [PATCH 01/17] Significantly improved CAD fetch and render processes --- src/app/components/cad/editor/index.tsx | 8 +-- src/app/components/cad/gallery/index.tsx | 4 +- src/app/hooks/features/cads/queryOptions.ts | 36 ++++++++++++ .../hooks/features/cads/useBlobUrlRevoker.ts | 16 +++++ src/app/hooks/features/cads/useCadBlobUrl.ts | 58 ------------------- src/app/hooks/features/cads/useFetchCad.ts | 27 +++++++++ src/lib/cad/loader.ts | 52 +++++++---------- src/lib/cad/three-js.ts | 2 + 8 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 src/app/hooks/features/cads/queryOptions.ts create mode 100644 src/app/hooks/features/cads/useBlobUrlRevoker.ts delete mode 100644 src/app/hooks/features/cads/useCadBlobUrl.ts create mode 100644 src/app/hooks/features/cads/useFetchCad.ts diff --git a/src/app/components/cad/editor/index.tsx b/src/app/components/cad/editor/index.tsx index ecb8e90..2620733 100644 --- a/src/app/components/cad/editor/index.tsx +++ b/src/app/components/cad/editor/index.tsx @@ -2,14 +2,14 @@ import { useQuery } from '@customcads/react-sdk'; import { AppError } from '@/types/errors'; import { getCadType } from '@/lib/cad'; import { useEditorStore } from '@/app/hooks/stores/useEditorStore'; -import { useCadBlobUrl } from '@/app/hooks/features/cads/useCadBlobUrl'; +import { useFetchCad } from '@/app/hooks/features/cads/useFetchCad'; import { useTextures } from '@/app/hooks/features/materials/useTextures'; import Loader from '@/app/components/loading'; import EditorThreeJS from './threejs'; type Props = { cadId: string }; const EditorCad = ({ cadId }: Props) => { - const { blobUrl: cadBlobUrl, progress } = useCadBlobUrl(cadId, 'Product'); + const { blobUrl, progress } = useFetchCad(cadId, 'Product'); const { data: cad } = useQuery(({ cads }) => cads.single({ id: cadId })); const materialId = useEditorStore(cadId, (state) => state.materialId); @@ -23,7 +23,7 @@ const EditorCad = ({ cadId }: Props) => { tip: 'Reload this page or clear your browser cache.', }); - if (!cad || !cadBlobUrl || !texture) { + if (!cad || !blobUrl || !texture) { return ; } @@ -33,7 +33,7 @@ const EditorCad = ({ cadId }: Props) => { texture={texture} cad={{ id: cadId, - blobUrl: cadBlobUrl, + blobUrl: blobUrl, type: getCadType(cad.contentType as never), coords: { cam: cad.camCoordinates, diff --git a/src/app/components/cad/gallery/index.tsx b/src/app/components/cad/gallery/index.tsx index 9324bb9..b359f99 100644 --- a/src/app/components/cad/gallery/index.tsx +++ b/src/app/components/cad/gallery/index.tsx @@ -1,12 +1,12 @@ import { useQuery } from '@customcads/react-sdk'; import { getCadType } from '@/lib/cad'; -import { useCadBlobUrl } from '@/app/hooks/features/cads/useCadBlobUrl'; +import { useFetchCad } from '@/app/hooks/features/cads/useFetchCad'; import Loader from '@/app/components/loading'; import GalleryThreeJS from './threejs'; const GalleryCad = ({ cadId }: { cadId: string }) => { const { data: cad } = useQuery(({ cads }) => cads.single({ id: cadId })); - const { blobUrl, progress } = useCadBlobUrl(cadId, 'Product'); + const { blobUrl, progress } = useFetchCad(cadId, 'Product'); return (
diff --git a/src/app/hooks/features/cads/queryOptions.ts b/src/app/hooks/features/cads/queryOptions.ts new file mode 100644 index 0000000..5ecb590 --- /dev/null +++ b/src/app/hooks/features/cads/queryOptions.ts @@ -0,0 +1,36 @@ +import { DownloadResponse } from '@customcads/react-sdk'; +import { fetchFile } from '@/lib/utils'; + +type Props = { + keys: unknown[]; + enabled?: boolean; + getDownloadUrl: () => Promise; + onProgress: (progress: number) => void; +}; +export const fetchCad = ({ + keys, + enabled, + getDownloadUrl, + onProgress, +}: Props) => ({ + queryKey: ['fetch-cad', ...keys, getDownloadUrl, onProgress], + queryFn: async () => { + const { length, response } = await fetchFile(await getDownloadUrl()); + + const reader = response.body?.getReader()!; + const parts: BlobPart[] = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + onProgress(value.length / length); + } + + const blob = new Blob(parts); + return URL.createObjectURL(blob); + }, + enabled, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, +}); diff --git a/src/app/hooks/features/cads/useBlobUrlRevoker.ts b/src/app/hooks/features/cads/useBlobUrlRevoker.ts new file mode 100644 index 0000000..238b0f2 --- /dev/null +++ b/src/app/hooks/features/cads/useBlobUrlRevoker.ts @@ -0,0 +1,16 @@ +import { useRef, useEffect } from 'react'; + +export const useBlobUrlRevoker = (blobUrl: string | undefined) => { + const prevBlobUrlRef = useRef(null); + + const revokeUrl = () => { + if (prevBlobUrlRef.current) { + URL.revokeObjectURL(prevBlobUrlRef.current); + } + }; + + useEffect(() => { + if (prevBlobUrlRef.current !== blobUrl) revokeUrl(); + prevBlobUrlRef.current = blobUrl ?? null; + }, [blobUrl]); +}; diff --git a/src/app/hooks/features/cads/useCadBlobUrl.ts b/src/app/hooks/features/cads/useCadBlobUrl.ts deleted file mode 100644 index 1306c8b..0000000 --- a/src/app/hooks/features/cads/useCadBlobUrl.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { DownloadRequest, useMutation } from '@customcads/react-sdk'; -import { fetchFile } from '@/lib/utils'; - -export const useCadBlobUrl = ( - cadId: DownloadRequest['id'] | undefined, - relationType: DownloadRequest['relationType'], -) => { - const { mutateAsync: downloadUrl } = useMutation( - ({ cads }) => cads.download, - ); - const [blobUrl, setBlobUrl] = useState(null); - const [progress, setProgress] = useState(0); - const isFetchingRef = useRef(false); - - const fetch = async (id: string) => { - isFetchingRef.current = true; - - const { length, response } = await fetchFile( - await downloadUrl({ id, relationType }), - ); - - const reader = response.body?.getReader()!; - const parts: BlobPart[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - parts.push(value); - setProgress((prev) => prev + value.length / length); - } - - const blob = new Blob(parts); - setBlobUrl(URL.createObjectURL(blob)); - - isFetchingRef.current = false; - }; - - const revokeUrl = (url: string | null) => { - if (url) URL.revokeObjectURL(url); - }; - - useEffect(() => { - if (cadId && isFetchingRef.current === false) { - revokeUrl(blobUrl); - fetch(cadId); - } - - return () => { - setBlobUrl((prevBlobUrl) => { - revokeUrl(prevBlobUrl); - return null; - }); - }; - }, [cadId, relationType]); - - return { blobUrl, progress }; -}; diff --git a/src/app/hooks/features/cads/useFetchCad.ts b/src/app/hooks/features/cads/useFetchCad.ts new file mode 100644 index 0000000..a63afe8 --- /dev/null +++ b/src/app/hooks/features/cads/useFetchCad.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; +import { useMutation, type DownloadRequest } from '@customcads/react-sdk'; +import { useQuery } from '@tanstack/react-query'; +import { useBlobUrlRevoker } from './useBlobUrlRevoker'; +import * as queryOptions from './queryOptions'; + +export const useFetchCad = ( + cadId: DownloadRequest['id'] | undefined, + relationType: DownloadRequest['relationType'], +) => { + const [progress, setProgress] = useState(0); + const { mutateAsync: downloadUrl } = useMutation( + ({ cads }) => cads.download, + ); + + const options = queryOptions.fetchCad({ + keys: [cadId, relationType], + enabled: !!cadId, + getDownloadUrl: () => downloadUrl({ id: cadId!, relationType }), + onProgress: (progress) => setProgress((prev) => prev + progress), + }); + + const { data: blobUrl } = useQuery(options); + useBlobUrlRevoker(blobUrl); + + return { blobUrl, progress }; +}; diff --git a/src/lib/cad/loader.ts b/src/lib/cad/loader.ts index 4fc567a..6e23800 100644 --- a/src/lib/cad/loader.ts +++ b/src/lib/cad/loader.ts @@ -9,48 +9,38 @@ const lock = (cad: Group) => { cad.position.sub(center); }; -export const gltf = ( +export const gltf = async ( scene: Scene, url: string, callback?: (cad: Group) => void, progress?: (percentage: number) => void, ) => { - new GLTFLoader().load( - url, - (cad) => { - lock(cad.scene); - if (callback) callback(cad.scene); - scene.add(cad.scene); - }, - (e) => { - const percentage = e.loaded / e.total; - if (progress) progress(percentage); - }, - ); + const cad = await new GLTFLoader().loadAsync(url, ({ loaded, total }) => { + const percentage = loaded / total; + if (progress) progress(percentage); + }); + + lock(cad.scene); + if (callback) callback(cad.scene); + scene.add(cad.scene); }; -export const stl = ( +export const stl = async ( scene: Scene, url: string, callback?: (cad: Group) => void, progress?: (percentage: number) => void, ) => { - new STLLoader().load( - url, - (cad) => { - const group = new Group(); - cad.center(); - group.add( - new Mesh(cad, new MeshStandardMaterial({ color: 0xaaaaaa })), - ); + const cad = await new STLLoader().loadAsync(url, ({ loaded, total }) => { + const percentage = loaded / total; + if (progress) progress(percentage); + }); + + const group = new Group(); + cad.center(); + group.add(new Mesh(cad, new MeshStandardMaterial({ color: 0xaaaaaa }))); - lock(group); - if (callback) callback(group); - scene.add(group); - }, - (e) => { - const percentage = e.loaded / e.total; - if (progress) progress(percentage); - }, - ); + lock(group); + if (callback) callback(group); + scene.add(group); }; diff --git a/src/lib/cad/three-js.ts b/src/lib/cad/three-js.ts index a460b11..ab9c162 100644 --- a/src/lib/cad/three-js.ts +++ b/src/lib/cad/three-js.ts @@ -139,6 +139,8 @@ export const initThreeJS = ( window.removeEventListener('resize', resizeHandler); controls.dispose(); + renderer.getContext().getExtension('WEBGL_lose_context')?.loseContext(); + renderer.dispose(); renderer.domElement.remove(); From 60b897070a77766b586beef5a35412d0fed034f9 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Tue, 21 Jul 2026 13:47:52 +0300 Subject: [PATCH 02/17] Tiny hooks refactorings --- .../hooks/features/carts/useCartItemEditor.ts | 23 ++++++++----------- .../customizations/useCustomizationCreator.ts | 8 ++++--- .../idempotency-keys/useIdempotencyKeys.ts | 10 ++++---- src/app/pages/public/editor/index.tsx | 2 +- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/app/hooks/features/carts/useCartItemEditor.ts b/src/app/hooks/features/carts/useCartItemEditor.ts index 225a89d..c3e0620 100644 --- a/src/app/hooks/features/carts/useCartItemEditor.ts +++ b/src/app/hooks/features/carts/useCartItemEditor.ts @@ -5,29 +5,26 @@ import { useCartStore } from '@/app/hooks/stores/useCartStore'; import { useCustomizationCreator } from '@/app/hooks/features/customizations/useCustomizationCreator'; import { useCartUpdates } from './useCartUpdates'; -export const useCartItemEditor = (productId: string) => { +export const useCartItemEditor = (productId: string, volume: number) => { const { items } = useCartStore(); const itemsLoaded = !!items; const item = items?.find((i) => i.productId === productId); const updates = useCartUpdates(); const addItemIfMissing = (customizationId: string) => { - if (!item) { - updates.cart.add({ - productId, - quantity: 1, - forDelivery: true, - customizationId: customizationId, - }); - return; - } + if (item) + return { otherwise: (cb: (item: CartItem) => void) => cb(item) }; - return { - otherwise: (callback: (item: CartItem) => void) => callback(item), - }; + updates.cart.add({ + productId, + quantity: 1, + forDelivery: true, + customizationId: customizationId, + }); }; const { customization, edit: editCustomization } = useCustomizationCreator( + volume, item, itemsLoaded, ); diff --git a/src/app/hooks/features/customizations/useCustomizationCreator.ts b/src/app/hooks/features/customizations/useCustomizationCreator.ts index 800447d..74cdb96 100644 --- a/src/app/hooks/features/customizations/useCustomizationCreator.ts +++ b/src/app/hooks/features/customizations/useCustomizationCreator.ts @@ -6,6 +6,7 @@ import { useIdempotencyKeys } from '@/app/hooks/features/idempotency-keys/useIde import { INFILL } from '@/app/constants/threejs'; export const useCustomizationCreator = ( + volume: number, item?: CartItem, enableCreation?: boolean, ) => { @@ -16,8 +17,9 @@ export const useCustomizationCreator = ( edit: useMutation(({ customizations }) => customizations.edit), }; - const itemCustomizationId = item?.forDelivery ? item.customizationId : null; - const customizationId = mutations.create.data?.id ?? itemCustomizationId; + const customizationId = + mutations.create.data?.id ?? + (item?.forDelivery ? item.customizationId : null); const { data: customization, error: error } = useQuery( ({ customizations }) => customizations.single({ id: customizationId! }), @@ -35,7 +37,7 @@ export const useCustomizationCreator = ( color: '#ffffff', infill: INFILL.min, scale: 100 / 100, - volume: 0, + volume, }); } }; diff --git a/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts b/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts index c350e27..4c03429 100644 --- a/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts +++ b/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts @@ -1,10 +1,10 @@ import { useRef } from 'react'; import { v4 as uuidv4 } from 'uuid'; -export const useIdempotencyKeys = ( - names: ExactNames, +export const useIdempotencyKeys = ( + allNames: AllNames, ) => { - type Name = ExactNames[number]; + type Name = AllNames[number]; type SomeNames = readonly Name[]; const generate = (names: SomeNames) => @@ -14,13 +14,13 @@ export const useIdempotencyKeys = ( >; const keysRef = useRef>(null); - keysRef.current ??= generate(names); + keysRef.current ??= generate(allNames); return { idempotencyKeys: keysRef.current, refreshKeys: (namesToRefresh?: SomeNames) => { if (!namesToRefresh) { - keysRef.current = generate(names); + keysRef.current = generate(allNames); return; } diff --git a/src/app/pages/public/editor/index.tsx b/src/app/pages/public/editor/index.tsx index 536d2ac..b334b5a 100644 --- a/src/app/pages/public/editor/index.tsx +++ b/src/app/pages/public/editor/index.tsx @@ -30,7 +30,7 @@ const Editor = () => { cads.single({ id: product.cadId }), ); - const { customization, save } = useCartItemEditor(product.id); + const { customization, save } = useCartItemEditor(product.id, cad.volume); const { scale, size } = { scale: useEditorStore(product.cadId, (state) => state.scale), size: useEditorStore(product.cadId, (state) => state.size), From 7f10d5bf2360b7b6a5990f7f4d576c3a632915c7 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Thu, 23 Jul 2026 04:48:48 +0300 Subject: [PATCH 03/17] UI bugs: Home page: Improved design responsiveness for Popular Products Cart page: Fixed