|
1 | | -import { useEffect, useRef, useState } from 'react'; |
2 | | -import * as THREE from 'three'; |
| 1 | +import { useMemo } from 'react'; |
3 | 2 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; |
4 | 3 | import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; |
5 | | -import { LngLatLike } from 'maplibre-gl'; |
6 | | -import { useThree } from '../ThreeContext'; |
7 | | -import ThreejsUtils from '../../lib/ThreejsUtils'; |
8 | | -import MlTransformControls from '../MlTransformControls'; |
| 4 | +import { useThreeModel, UseThreeModelProps, ModelLoader } from '../../hooks/useThreeModel'; |
9 | 5 |
|
10 | 6 | /** |
11 | | - * Renders obj or gltf 3D Models on the MapLibreMap referenced by props.mapId |
| 7 | + * Renders obj or gltf 3D Models on the MapLibreMap |
12 | 8 | * |
13 | 9 | * @component |
14 | 10 | */ |
15 | 11 |
|
16 | | -export interface MlThreeModelLayerProps { |
| 12 | +export type MlThreeModelLayerProps = Omit<UseThreeModelProps, 'loaders'> & { |
17 | 13 | mapId?: string; |
18 | | - url: string; |
19 | | - position?: { x: number; y: number; z: number }; |
20 | | - mapPosition?: LngLatLike; |
21 | | - altitude?: number; |
22 | | - rotation?: { x: number; y: number; z: number }; |
23 | | - scale?: { x: number; y: number; z: number } | number; |
24 | | - enableTransformControls?: boolean; |
25 | | - transformMode?: 'translate' | 'rotate' | 'scale'; |
26 | | - onTransformChange?: (object: THREE.Object3D) => void; |
27 | | - init?: () => void; |
28 | | - onDone?: () => void; |
29 | | -} |
| 14 | +}; |
30 | 15 |
|
31 | 16 | const MlThreeModelLayer = (props: MlThreeModelLayerProps) => { |
32 | | - const { |
| 17 | + const { url, position, transform, init, onDone, customLoaders } = props; |
| 18 | + |
| 19 | + const loaders = useMemo<Record<string, ModelLoader>>( |
| 20 | + () => ({ |
| 21 | + gltf: (url, onLoad) => { |
| 22 | + const loader = new GLTFLoader(); |
| 23 | + loader.load(url, (gltf) => onLoad(gltf.scene)); |
| 24 | + }, |
| 25 | + glb: (url, onLoad) => { |
| 26 | + const loader = new GLTFLoader(); |
| 27 | + loader.load(url, (gltf) => onLoad(gltf.scene)); |
| 28 | + }, |
| 29 | + obj: (url, onLoad) => { |
| 30 | + const loader = new OBJLoader(); |
| 31 | + loader.load(url, (obj) => onLoad(obj)); |
| 32 | + }, |
| 33 | + }), |
| 34 | + [] |
| 35 | + ); |
| 36 | + |
| 37 | + useThreeModel({ |
33 | 38 | url, |
34 | 39 | position, |
35 | | - mapPosition, |
36 | | - altitude, |
37 | | - rotation, |
38 | | - scale, |
39 | | - enableTransformControls, |
40 | | - transformMode, |
41 | | - onTransformChange, |
| 40 | + transform, |
42 | 41 | init, |
43 | 42 | onDone, |
44 | | - } = props; |
45 | | - const { scene, worldMatrixInv } = useThree(); |
46 | | - const modelRef = useRef<THREE.Object3D | undefined>(undefined); |
47 | | - const [model, setModel] = useState<THREE.Object3D | undefined>(undefined); |
48 | | - |
49 | | - // Use refs for callbacks to avoid re-triggering the effect when they change |
50 | | - const initRef = useRef(init); |
51 | | - const onDoneRef = useRef(onDone); |
52 | | - initRef.current = init; |
53 | | - onDoneRef.current = onDone; |
54 | | - |
55 | | - const transformRef = useRef({ position, mapPosition, altitude, rotation, scale }); |
56 | | - transformRef.current = { position, mapPosition, altitude, rotation, scale }; |
57 | | - const worldMatrixInvRef = useRef(worldMatrixInv); |
58 | | - worldMatrixInvRef.current = worldMatrixInv; |
59 | | - |
60 | | - useEffect(() => { |
61 | | - if (!scene) return; |
62 | | - |
63 | | - if (typeof initRef.current === 'function') { |
64 | | - initRef.current(); |
65 | | - } |
66 | | - |
67 | | - const extension = url.split('.').pop()?.toLowerCase(); |
68 | | - |
69 | | - const onLoad = (object: THREE.Object3D) => { |
70 | | - const { position, mapPosition, altitude, rotation, scale } = transformRef.current; |
71 | | - const worldMatrixInv = worldMatrixInvRef.current; |
72 | | - |
73 | | - if (mapPosition && worldMatrixInv) { |
74 | | - const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude); |
75 | | - object.position.set(scenePos.x, scenePos.y, scenePos.z); |
76 | | - } else if (position) { |
77 | | - object.position.set(position.x, position.y, position.z); |
78 | | - } |
79 | | - |
80 | | - if (rotation) { |
81 | | - object.rotation.set(rotation.x, rotation.y, rotation.z); |
82 | | - } |
83 | | - if (scale) { |
84 | | - if (typeof scale === 'number') { |
85 | | - object.scale.set(scale, scale, scale); |
86 | | - } else { |
87 | | - object.scale.set(scale.x, scale.y, scale.z); |
88 | | - } |
89 | | - } |
90 | | - |
91 | | - modelRef.current = object; |
92 | | - scene.add(object); |
93 | | - setModel(object); |
94 | | - if (typeof onDoneRef.current === 'function') { |
95 | | - onDoneRef.current(); |
96 | | - } |
97 | | - }; |
98 | | - |
99 | | - if (extension === 'glb' || extension === 'gltf') { |
100 | | - const loader = new GLTFLoader(); |
101 | | - loader.load(url, (gltf) => { |
102 | | - onLoad(gltf.scene); |
103 | | - }); |
104 | | - } else if (extension === 'obj') { |
105 | | - const loader = new OBJLoader(); |
106 | | - loader.load(url, (obj) => { |
107 | | - onLoad(obj); |
108 | | - }); |
109 | | - } else { |
110 | | - console.warn('MlThreeModelLayer: Unsupported file extension', extension); |
111 | | - } |
112 | | - |
113 | | - return () => { |
114 | | - if (modelRef.current) { |
115 | | - scene.remove(modelRef.current); |
116 | | - modelRef.current = undefined; |
117 | | - setModel(undefined); |
118 | | - } |
119 | | - }; |
120 | | - }, [scene, url]); |
121 | | - |
122 | | - useEffect(() => { |
123 | | - if (!model) return; |
124 | | - |
125 | | - // Handle position: mapPosition takes precedence over position |
126 | | - if (mapPosition && worldMatrixInv) { |
127 | | - const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude); |
128 | | - model.position.set(scenePos.x, scenePos.y, scenePos.z); |
129 | | - } else if (position) { |
130 | | - model.position.set(position.x, position.y, position.z); |
131 | | - } |
132 | | - |
133 | | - if (rotation) { |
134 | | - model.rotation.set(rotation.x, rotation.y, rotation.z); |
135 | | - } |
136 | | - if (scale) { |
137 | | - if (typeof scale === 'number') { |
138 | | - model.scale.set(scale, scale, scale); |
139 | | - } else { |
140 | | - model.scale.set(scale.x, scale.y, scale.z); |
141 | | - } |
142 | | - } |
143 | | - }, [model, position, mapPosition, altitude, rotation, scale, worldMatrixInv]); |
| 43 | + loaders, |
| 44 | + customLoaders, |
| 45 | + }); |
144 | 46 |
|
145 | | - if (enableTransformControls && model) { |
146 | | - return ( |
147 | | - <MlTransformControls target={model} mode={transformMode} onObjectChange={onTransformChange} /> |
148 | | - ); |
149 | | - } |
150 | 47 | return null; |
151 | 48 | }; |
152 | 49 |
|
|
0 commit comments