@vctrl/hooks 0.0.8 → 0.0.9-2
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/.babelrc +12 -0
- package/.eslintrc.json +18 -0
- package/package.json +1 -4
- package/project.json +16 -0
- package/src/index.ts +1 -0
- package/src/use-load-model/event-system.ts +29 -0
- package/src/use-load-model/file-type-hooks/index.ts +2 -0
- package/src/use-load-model/file-type-hooks/use-load-binary.ts +74 -0
- package/src/use-load-model/file-type-hooks/use-load-gltf.ts +115 -0
- package/src/use-load-model/index.ts +3 -0
- package/src/use-load-model/loaders/create-gltf-loader.ts +20 -0
- package/src/use-load-model/loaders/create-usdz-loader.ts +9 -0
- package/src/use-load-model/loaders/index.ts +2 -0
- package/src/use-load-model/model-context.tsx +18 -0
- package/src/use-load-model/state.ts +33 -0
- package/src/use-load-model/types.ts +53 -0
- package/src/use-load-model/use-load-model.ts +111 -0
- package/src/use-load-model/utils/array-buffer-to-base64.ts +20 -0
- package/src/use-load-model/utils/index.ts +2 -0
- package/src/use-load-model/utils/read-directory.ts +31 -0
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +25 -0
- package/vite.config.ts +59 -0
package/.babelrc
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
7
|
+
"rules": {}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"files": ["*.ts", "*.tsx"],
|
|
11
|
+
"rules": {}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"files": ["*.js", "*.jsx"],
|
|
15
|
+
"rules": {}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.9-2",
|
|
3
3
|
"name": "@vctrl/hooks",
|
|
4
4
|
"description": "vctrl/hooks is a React hooks package designed to simplify 3D model loading and management within React applications. It's part of the vectreal-core ecosystem and is primarily used in the vctrl/viewer React component and the official website application.",
|
|
5
5
|
"bugs": {
|
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
|
-
"files": [
|
|
38
|
-
"."
|
|
39
|
-
],
|
|
40
37
|
"exports": {
|
|
41
38
|
"./use-load-model": {
|
|
42
39
|
"import": "./use-load-model.es.js",
|
package/project.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vctrl/hooks",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/hooks/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/vite:build",
|
|
10
|
+
"options": {
|
|
11
|
+
"outputPath": "dist/packages/vctrl/hooks",
|
|
12
|
+
"configFile": "packages/hooks/vite.config.ts"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './use-load-model';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EventData, EventHandler, EventTypes } from './types';
|
|
2
|
+
|
|
3
|
+
const listeners = {} as Record<EventTypes, EventHandler<EventTypes>[]>;
|
|
4
|
+
const eventSystem = {
|
|
5
|
+
emit<T extends EventTypes>(event: T, data: EventData[T]): void {
|
|
6
|
+
const handlers = listeners[event];
|
|
7
|
+
if (handlers) {
|
|
8
|
+
handlers.forEach((handler) => handler(data));
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
on<T extends EventTypes>(event: T, handler: EventHandler<T>): void {
|
|
13
|
+
if (!listeners[event]) {
|
|
14
|
+
listeners[event] = [];
|
|
15
|
+
}
|
|
16
|
+
listeners[event].push(handler as EventHandler<EventTypes>);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
off<T extends EventTypes>(event: T, handler: EventHandler<T>): void {
|
|
20
|
+
const handlers = listeners[event];
|
|
21
|
+
if (handlers) {
|
|
22
|
+
listeners[event] = handlers.filter(
|
|
23
|
+
(h) => h !== handler,
|
|
24
|
+
) as EventHandler<EventTypes>[];
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default eventSystem;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Action, ModelFileTypes } from '../types';
|
|
4
|
+
import { createGltfLoader, createUsdzLoader } from '../loaders';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to load binary files
|
|
8
|
+
*
|
|
9
|
+
* @param dispatch - The dispatch function from the useReducer hook
|
|
10
|
+
* @returns An object containing the loadBinary function
|
|
11
|
+
*/
|
|
12
|
+
function useLoadBinary(dispatch: React.Dispatch<Action>) {
|
|
13
|
+
/**
|
|
14
|
+
* Function to read binary files
|
|
15
|
+
*
|
|
16
|
+
* @param file - The binary file to read
|
|
17
|
+
* @param fileType - The type of file to read
|
|
18
|
+
* @param onProgress - The function to call when progress is made
|
|
19
|
+
* @returns {void}
|
|
20
|
+
*/
|
|
21
|
+
const loadBinary = useCallback(
|
|
22
|
+
(file: File, fileType: ModelFileTypes, onProgress: () => void) => {
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
|
|
25
|
+
reader.onload = async (event: ProgressEvent<FileReader>) => {
|
|
26
|
+
if (event.target?.result) {
|
|
27
|
+
const arrayBuffer = event.target.result as ArrayBuffer;
|
|
28
|
+
|
|
29
|
+
if (fileType === ModelFileTypes.glb) {
|
|
30
|
+
const gltfLoader = createGltfLoader();
|
|
31
|
+
|
|
32
|
+
gltfLoader.parse(arrayBuffer, '', (gltf) => {
|
|
33
|
+
dispatch({
|
|
34
|
+
type: 'SET_FILE',
|
|
35
|
+
payload: {
|
|
36
|
+
model: gltf.scene,
|
|
37
|
+
type: ModelFileTypes.glb,
|
|
38
|
+
name: file.name,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} else if (fileType === ModelFileTypes.usdz) {
|
|
43
|
+
const usdzLoader = createUsdzLoader();
|
|
44
|
+
const model = usdzLoader.parse(arrayBuffer);
|
|
45
|
+
|
|
46
|
+
dispatch({
|
|
47
|
+
type: 'SET_FILE',
|
|
48
|
+
payload: {
|
|
49
|
+
model: model,
|
|
50
|
+
type: ModelFileTypes.usdz,
|
|
51
|
+
name: file.name,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onProgress(); // Call progress callback when binary file is loaded
|
|
57
|
+
dispatch({ type: 'SET_FILE_LOADING', payload: false });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
reader.onerror = (error) => {
|
|
62
|
+
console.error('Error reading file:', error);
|
|
63
|
+
dispatch({ type: 'SET_FILE_LOADING', payload: false });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
reader.readAsArrayBuffer(file);
|
|
67
|
+
},
|
|
68
|
+
[dispatch],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { loadBinary };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default useLoadBinary;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Action, ModelFileTypes, ReducedGltf } from '../types';
|
|
4
|
+
import { arrayBufferToBase64 } from '../utils';
|
|
5
|
+
import { createGltfLoader } from '../loaders';
|
|
6
|
+
|
|
7
|
+
function useLoadGltf(dispatch: React.Dispatch<Action>) {
|
|
8
|
+
const embedExternalResources = useCallback(
|
|
9
|
+
async (
|
|
10
|
+
gltfContent: ReducedGltf,
|
|
11
|
+
otherFiles: File[],
|
|
12
|
+
onProgress: (progress: number) => void,
|
|
13
|
+
) => {
|
|
14
|
+
const fileMap = new Map(otherFiles.map((file) => [file.name, file]));
|
|
15
|
+
const totalFiles =
|
|
16
|
+
(gltfContent.buffers?.length || 0) + (gltfContent.images?.length || 0);
|
|
17
|
+
let processedFiles = 0;
|
|
18
|
+
|
|
19
|
+
const updateProgress = () => {
|
|
20
|
+
processedFiles++;
|
|
21
|
+
onProgress((processedFiles / totalFiles) * 100);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Embed buffers
|
|
25
|
+
if (gltfContent.buffers) {
|
|
26
|
+
for (let i = 0; i < gltfContent.buffers.length; i++) {
|
|
27
|
+
const buffer = gltfContent.buffers[i];
|
|
28
|
+
if (!buffer.uri || buffer.uri.startsWith('data:')) {
|
|
29
|
+
updateProgress();
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fileName = buffer.uri.split('/').pop() || '';
|
|
34
|
+
const file = fileMap.get(fileName);
|
|
35
|
+
if (file) {
|
|
36
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
37
|
+
const base64 = await arrayBufferToBase64(arrayBuffer);
|
|
38
|
+
buffer.uri = `data:application/octet-stream;base64,${base64}`;
|
|
39
|
+
}
|
|
40
|
+
updateProgress();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Embed images
|
|
45
|
+
if (gltfContent.images) {
|
|
46
|
+
for (let i = 0; i < gltfContent.images.length; i++) {
|
|
47
|
+
const image = gltfContent.images[i];
|
|
48
|
+
if (!image.uri || image.uri.startsWith('data:')) {
|
|
49
|
+
updateProgress();
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fileName = image.uri.split('/').pop() || '';
|
|
54
|
+
const file = fileMap.get(fileName);
|
|
55
|
+
if (file) {
|
|
56
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
57
|
+
const base64 = await arrayBufferToBase64(arrayBuffer);
|
|
58
|
+
const mimeType = file.type || 'image/png';
|
|
59
|
+
image.uri = `data:${mimeType};base64,${base64}`;
|
|
60
|
+
}
|
|
61
|
+
updateProgress();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return gltfContent;
|
|
66
|
+
},
|
|
67
|
+
[],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const loadGltf = useCallback(
|
|
71
|
+
(
|
|
72
|
+
gltfFile: File,
|
|
73
|
+
otherFiles: File[],
|
|
74
|
+
onProgress: (progress: number) => void,
|
|
75
|
+
) => {
|
|
76
|
+
const reader = new FileReader();
|
|
77
|
+
reader.onload = async (e) => {
|
|
78
|
+
const gltfContent = JSON.parse(e.target?.result as string);
|
|
79
|
+
onProgress(10); // Initial progress after parsing GLTF
|
|
80
|
+
|
|
81
|
+
const modifiedGLTF = await embedExternalResources(
|
|
82
|
+
gltfContent,
|
|
83
|
+
otherFiles,
|
|
84
|
+
(embeddingProgress) => {
|
|
85
|
+
// Map embedding progress to 10-90% range
|
|
86
|
+
onProgress(10 + embeddingProgress * 0.8);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const gltfLoader = createGltfLoader();
|
|
91
|
+
|
|
92
|
+
gltfLoader.parse(JSON.stringify(modifiedGLTF), '', (gltf) => {
|
|
93
|
+
dispatch({
|
|
94
|
+
type: 'SET_FILE',
|
|
95
|
+
payload: {
|
|
96
|
+
model: gltf.scene,
|
|
97
|
+
type: ModelFileTypes.gltf,
|
|
98
|
+
name: gltfFile.name,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
onProgress(100); // Final progress
|
|
104
|
+
dispatch({ type: 'SET_FILE_LOADING', payload: false });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
reader.readAsText(gltfFile);
|
|
108
|
+
},
|
|
109
|
+
[dispatch, embedExternalResources],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return { loadGltf };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default useLoadGltf;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WebGLRenderer } from 'three';
|
|
2
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
3
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
|
4
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
|
|
5
|
+
|
|
6
|
+
function createGltfLoader() {
|
|
7
|
+
const dracoLoader = new DRACOLoader();
|
|
8
|
+
dracoLoader.setDecoderPath('/draco/');
|
|
9
|
+
|
|
10
|
+
const ktxLoader = new KTX2Loader();
|
|
11
|
+
ktxLoader.setTranscoderPath('/draco/');
|
|
12
|
+
|
|
13
|
+
const gltfLoader = new GLTFLoader()
|
|
14
|
+
.setDRACOLoader(dracoLoader)
|
|
15
|
+
.setKTX2Loader(ktxLoader.detectSupport(new WebGLRenderer()));
|
|
16
|
+
|
|
17
|
+
return gltfLoader;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default createGltfLoader;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import useLoadModel from './use-load-model';
|
|
3
|
+
|
|
4
|
+
const ModelContext = createContext({} as ReturnType<typeof useLoadModel>);
|
|
5
|
+
|
|
6
|
+
const ModelProvider = ({ children }: React.PropsWithChildren) => {
|
|
7
|
+
const value = useLoadModel();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<ModelContext.Provider value={value}>{children}</ModelContext.Provider>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const useModelContext = () => {
|
|
15
|
+
return useContext(ModelContext);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { ModelContext, ModelProvider, useModelContext };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Action, ModelFileTypes, State } from './types';
|
|
2
|
+
|
|
3
|
+
// Initial state
|
|
4
|
+
export const initialState: State = {
|
|
5
|
+
file: null,
|
|
6
|
+
isFileLoading: false,
|
|
7
|
+
progress: 0,
|
|
8
|
+
supportedFileTypes: Object.values(ModelFileTypes),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reducer function for the useReadModelFiles hook
|
|
13
|
+
*
|
|
14
|
+
* @param state - The current state of the reducer
|
|
15
|
+
* @param action - The action to be performed
|
|
16
|
+
* @returns The updated state
|
|
17
|
+
*/
|
|
18
|
+
function reducer(state: State, action: Action): State {
|
|
19
|
+
switch (action.type) {
|
|
20
|
+
case 'SET_FILE':
|
|
21
|
+
return { ...state, file: action.payload };
|
|
22
|
+
case 'SET_FILE_LOADING':
|
|
23
|
+
return { ...state, isFileLoading: action.payload };
|
|
24
|
+
case 'SET_PROGRESS':
|
|
25
|
+
return { ...state, progress: action.payload };
|
|
26
|
+
case 'RESET_STATE':
|
|
27
|
+
return { ...initialState };
|
|
28
|
+
default:
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default reducer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Object3D } from 'three';
|
|
2
|
+
|
|
3
|
+
export enum ModelFileTypes {
|
|
4
|
+
gltf = 'gltf',
|
|
5
|
+
glb = 'glb',
|
|
6
|
+
usdz = 'usdz',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type InputFileOrDirectory = (File | FileSystemDirectoryHandle)[];
|
|
10
|
+
|
|
11
|
+
export interface ReducedGltf {
|
|
12
|
+
images: { name: string; uri: string }[];
|
|
13
|
+
buffers: { bytelength: string; uri: string }[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModelFile {
|
|
17
|
+
model: Object3D;
|
|
18
|
+
type: ModelFileTypes;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Updated State interface
|
|
23
|
+
export interface State {
|
|
24
|
+
file: ModelFile | null;
|
|
25
|
+
isFileLoading: boolean;
|
|
26
|
+
progress: number;
|
|
27
|
+
supportedFileTypes: ModelFileTypes[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Updated Action types
|
|
31
|
+
export type Action =
|
|
32
|
+
| { type: 'SET_FILE'; payload: ModelFile }
|
|
33
|
+
| { type: 'SET_FILE_LOADING'; payload: boolean }
|
|
34
|
+
| { type: 'SET_PROGRESS'; payload: number }
|
|
35
|
+
| { type: 'RESET_STATE' };
|
|
36
|
+
|
|
37
|
+
// Updated Event types
|
|
38
|
+
export type EventTypes =
|
|
39
|
+
| 'MULTIPLE_3D_MODELS'
|
|
40
|
+
| 'UNSUPPORTED_FILE_TYPE'
|
|
41
|
+
| 'UPLOAD_PROGRESS'
|
|
42
|
+
| 'UPLOAD_COMPLETE';
|
|
43
|
+
|
|
44
|
+
// Updated event data types
|
|
45
|
+
export type EventData = {
|
|
46
|
+
MULTIPLE_3D_MODELS: File[];
|
|
47
|
+
UNSUPPORTED_FILE_TYPE: File[];
|
|
48
|
+
UPLOAD_PROGRESS: number;
|
|
49
|
+
UPLOAD_COMPLETE: State['file'];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Updated EventHandler type
|
|
53
|
+
export type EventHandler<T extends EventTypes> = (data: EventData[T]) => void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useReducer, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ModelFileTypes, InputFileOrDirectory } from './types';
|
|
4
|
+
import { useLoadBinary, useLoadGltf } from './file-type-hooks';
|
|
5
|
+
import { readDirectory } from './utils';
|
|
6
|
+
|
|
7
|
+
import eventSystem from './event-system';
|
|
8
|
+
import reducer, { initialState } from './state';
|
|
9
|
+
|
|
10
|
+
function useLoadModel() {
|
|
11
|
+
const uploadCompleteRef = useRef(false);
|
|
12
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
13
|
+
|
|
14
|
+
const { loadGltf } = useLoadGltf(dispatch);
|
|
15
|
+
const { loadBinary } = useLoadBinary(dispatch);
|
|
16
|
+
|
|
17
|
+
const getFileOfType = useCallback(
|
|
18
|
+
(files: File[], fileType: ModelFileTypes) =>
|
|
19
|
+
files.find((file) => file.name.endsWith('.' + fileType)),
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const updateProgress = useCallback((progress: number) => {
|
|
24
|
+
dispatch({ type: 'SET_PROGRESS', payload: progress });
|
|
25
|
+
eventSystem.emit('UPLOAD_PROGRESS', progress);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const processFiles = useCallback(
|
|
29
|
+
(files: File[]) => {
|
|
30
|
+
if (files.length === 0) return;
|
|
31
|
+
|
|
32
|
+
dispatch({ type: 'SET_FILE_LOADING', payload: true });
|
|
33
|
+
updateProgress(0);
|
|
34
|
+
uploadCompleteRef.current = false;
|
|
35
|
+
|
|
36
|
+
const gltfFile = getFileOfType(files, ModelFileTypes.gltf);
|
|
37
|
+
const glbFile = getFileOfType(files, ModelFileTypes.glb);
|
|
38
|
+
const usdzFile = getFileOfType(files, ModelFileTypes.usdz);
|
|
39
|
+
|
|
40
|
+
const supportedFiles = [gltfFile, glbFile, usdzFile].filter(
|
|
41
|
+
Boolean,
|
|
42
|
+
) as File[];
|
|
43
|
+
|
|
44
|
+
if (supportedFiles.length > 1) {
|
|
45
|
+
eventSystem.emit('MULTIPLE_3D_MODELS', supportedFiles);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const otherFiles = files.filter(
|
|
50
|
+
(file) => file !== gltfFile && file !== glbFile && file !== usdzFile,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const updateFileProgress = (progress: number) => {
|
|
54
|
+
updateProgress(progress);
|
|
55
|
+
|
|
56
|
+
if (progress === 100) {
|
|
57
|
+
uploadCompleteRef.current = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (gltfFile) {
|
|
62
|
+
loadGltf(gltfFile, otherFiles, updateFileProgress);
|
|
63
|
+
} else if (glbFile) {
|
|
64
|
+
loadBinary(glbFile, ModelFileTypes.glb, () => updateFileProgress(100));
|
|
65
|
+
} else if (usdzFile) {
|
|
66
|
+
loadBinary(usdzFile, ModelFileTypes.usdz, () =>
|
|
67
|
+
updateFileProgress(100),
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
eventSystem.emit('UNSUPPORTED_FILE_TYPE', files);
|
|
71
|
+
dispatch({ type: 'SET_FILE_LOADING', payload: false });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[updateProgress, getFileOfType, loadGltf, loadBinary],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const handleFileUpload = useCallback(
|
|
79
|
+
async (filesOrDirectories: InputFileOrDirectory) => {
|
|
80
|
+
const allFiles: File[] = [];
|
|
81
|
+
|
|
82
|
+
for (const item of filesOrDirectories) {
|
|
83
|
+
if (item instanceof File) {
|
|
84
|
+
allFiles.push(item);
|
|
85
|
+
} else if ('kind' in item && item.kind === 'directory') {
|
|
86
|
+
const directoryFiles = await readDirectory(item);
|
|
87
|
+
allFiles.push(...directoryFiles);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
processFiles(allFiles);
|
|
92
|
+
},
|
|
93
|
+
[processFiles],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (uploadCompleteRef.current && state.file) {
|
|
98
|
+
eventSystem.emit('UPLOAD_COMPLETE', state.file);
|
|
99
|
+
uploadCompleteRef.current = false;
|
|
100
|
+
}
|
|
101
|
+
}, [state.file]);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
on: eventSystem.on,
|
|
106
|
+
off: eventSystem.off,
|
|
107
|
+
handleFileUpload,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default useLoadModel;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Helper function to convert ArrayBuffer to base64
|
|
2
|
+
async function arrayBufferToBase64(arrayBuffer: ArrayBuffer) {
|
|
3
|
+
// Create a Blob from the ArrayBuffer
|
|
4
|
+
const blob = new Blob([arrayBuffer]);
|
|
5
|
+
|
|
6
|
+
// Use the FileReader to convert the Blob to a Base64 string
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
reader.onloadend = () => {
|
|
10
|
+
if (!reader?.result) return;
|
|
11
|
+
// Extract the Base64 string from the data URL
|
|
12
|
+
const base64String = (reader?.result as string).split(',')[1];
|
|
13
|
+
resolve(base64String);
|
|
14
|
+
};
|
|
15
|
+
reader.onerror = reject;
|
|
16
|
+
reader.readAsDataURL(blob);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default arrayBufferToBase64;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively reads a directory
|
|
3
|
+
*
|
|
4
|
+
* @param directoryHandle - The directory handle to read
|
|
5
|
+
* @returns - An array of file objects
|
|
6
|
+
*/
|
|
7
|
+
async function readDirectory(
|
|
8
|
+
directoryHandle: FileSystemDirectoryHandle,
|
|
9
|
+
): Promise<File[]> {
|
|
10
|
+
const files: File[] = [];
|
|
11
|
+
|
|
12
|
+
async function* getFilesRecursively(
|
|
13
|
+
entry: FileSystemDirectoryHandle,
|
|
14
|
+
): AsyncGenerator<File> {
|
|
15
|
+
for await (const [, handle] of entry) {
|
|
16
|
+
if (handle.kind === 'file') {
|
|
17
|
+
yield await (handle as FileSystemFileHandle).getFile();
|
|
18
|
+
} else if (handle.kind === 'directory') {
|
|
19
|
+
yield* getFilesRecursively(handle as FileSystemDirectoryHandle);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for await (const file of getFilesRecursively(directoryHandle)) {
|
|
25
|
+
files.push(file);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default readDirectory;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "react-jsx",
|
|
4
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"esModuleInterop": false,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"types": ["vite/client"]
|
|
12
|
+
},
|
|
13
|
+
"files": [],
|
|
14
|
+
"include": [],
|
|
15
|
+
"references": [
|
|
16
|
+
{
|
|
17
|
+
"path": "./tsconfig.lib.json"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"extends": "../../tsconfig.base.json"
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"isolatedModules": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"types": [
|
|
8
|
+
"node",
|
|
9
|
+
"@nx/react/typings/cssmodule.d.ts",
|
|
10
|
+
"@nx/react/typings/image.d.ts",
|
|
11
|
+
"vite/client"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"exclude": [
|
|
15
|
+
"**/*.spec.ts",
|
|
16
|
+
"**/*.test.ts",
|
|
17
|
+
"**/*.spec.tsx",
|
|
18
|
+
"**/*.test.tsx",
|
|
19
|
+
"**/*.spec.js",
|
|
20
|
+
"**/*.test.js",
|
|
21
|
+
"**/*.spec.jsx",
|
|
22
|
+
"**/*.test.jsx"
|
|
23
|
+
],
|
|
24
|
+
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
25
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/// <reference types='vitest' />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import dts from 'vite-plugin-dts';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
root: __dirname,
|
|
10
|
+
cacheDir: '../../node_modules/.vite/packages/@vctrl/hooks',
|
|
11
|
+
|
|
12
|
+
plugins: [
|
|
13
|
+
react(),
|
|
14
|
+
nxViteTsPaths(),
|
|
15
|
+
dts({
|
|
16
|
+
entryRoot: 'src',
|
|
17
|
+
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
// Uncomment this if you are using workers.
|
|
22
|
+
// worker: {
|
|
23
|
+
// plugins: [ nxViteTsPaths() ],
|
|
24
|
+
// },
|
|
25
|
+
|
|
26
|
+
// Configuration for building your library.
|
|
27
|
+
// See: https://vitejs.dev/guide/build.html#library-mode
|
|
28
|
+
build: {
|
|
29
|
+
outDir: '../../dist/packages/@vctrl/hooks',
|
|
30
|
+
emptyOutDir: true,
|
|
31
|
+
reportCompressedSize: true,
|
|
32
|
+
commonjsOptions: {
|
|
33
|
+
transformMixedEsModules: true,
|
|
34
|
+
},
|
|
35
|
+
lib: {
|
|
36
|
+
entry: {
|
|
37
|
+
index: path.resolve(__dirname, 'src/index.ts'),
|
|
38
|
+
'use-load-model': path.resolve(
|
|
39
|
+
__dirname,
|
|
40
|
+
'src/use-load-model/index.ts',
|
|
41
|
+
),
|
|
42
|
+
},
|
|
43
|
+
name: '@vctrl/hooks',
|
|
44
|
+
formats: ['es', 'cjs'],
|
|
45
|
+
fileName: (format, entry) => `${entry}.${format}.js`,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
rollupOptions: {
|
|
49
|
+
// External packages that should not be bundled into your library.
|
|
50
|
+
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
|
51
|
+
output: {
|
|
52
|
+
globals: {
|
|
53
|
+
react: 'React',
|
|
54
|
+
'react-dom': 'ReactDOM',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|