4545<script setup lang="ts">
4646import { ref , unref , onMounted , onBeforeUnmount , computed } from ' vue'
4747import {
48+ AmbientLight ,
49+ AxesHelper ,
4850 Scene ,
51+ Mesh ,
4952 PerspectiveCamera ,
53+ PointLight ,
5054 WebGLRenderer ,
5155 ACESFilmicToneMapping ,
5256 EquirectangularReflectionMapping ,
5357 Box3 ,
5458 Vector3 ,
5559 Euler ,
56- TextureLoader
60+ TextureLoader ,
61+ MeshPhongMaterial
5762} from ' three'
5863import WebGL from ' three/examples/jsm/capabilities/WebGL'
5964import { GLTFLoader } from ' three/examples/jsm/loaders/GLTFLoader'
65+ import { STLLoader } from ' three/examples/jsm/loaders/STLLoader'
66+ import { FBXLoader } from ' three/examples/jsm/loaders/FBXLoader'
67+ import { OBJLoader } from ' three/examples/jsm/loaders/OBJLoader'
6068import { OrbitControls } from ' three/examples/jsm/controls/OrbitControls'
6169import {
6270 AppLoadingSpinner ,
@@ -75,7 +83,7 @@ import PreviewControls from './components/PreviewControls.vue'
7583import { id as appId } from ' ../public/manifest.json'
7684
7785const environment = new URL (' ./assets/custom_light.jpg' , import .meta .url ).href
78- const supportExtensions = [' glb' ]
86+ const supportExtensions = [' glb' , ' stl ' , ' fbx ' , ' obj ' ]
7987
8088const router = useRouter ()
8189const route = useRoute ()
@@ -92,6 +100,7 @@ let iniCamPosition: Vector3 | null = null
92100let iniCamZPosition: number = 0
93101const iniCamRotation: Euler = new Euler (0 , 0 , 0 )
94102const animTimeoutSec = 1
103+ const debugIsEnabled = false
95104
96105// =====================
97106// props
@@ -137,13 +146,14 @@ onMounted(async () => {
137146
138147 // camera controls
139148 controls = new OrbitControls (camera , renderer .domElement )
140- controls .minDistance = 1
149+ controls .minDistance = 0
141150 controls .maxDistance = 100
142151
143152 // load environment texture
144153 try {
145154 await loadEnvironment ()
146- await renderModel ()
155+ await renderModel (unref (fileType ))
156+ loadLights ()
147157 } catch (e ) {
148158 cleanup3dScene ()
149159 hasError .value = true
@@ -182,6 +192,7 @@ const modelFiles = computed<Resource[]>(() => {
182192 return sortHelper (files , [{ name: unref (sortBy ) }], unref (sortBy ), unref (sortDir ))
183193})
184194const activeModelFile = computed (() => unref (modelFiles )[unref (activeIndex )])
195+ const fileType = computed (() => unref (activeModelFile )?.extension )
185196
186197// =====================
187198// methods
@@ -192,22 +203,65 @@ async function updateUrl() {
192203 unref (activeModelFile )
193204 )
194205}
206+
195207async function loadEnvironment() {
196208 const texture = await new TextureLoader ().loadAsync (environment )
197209 texture .mapping = EquirectangularReflectionMapping
198210 scene .environment = texture
199211}
200- async function renderModel() {
201- const model = await new GLTFLoader ().loadAsync (unref (currentUrl ), (xhr ) => {
212+
213+ const LoaderMap = {
214+ glb: GLTFLoader ,
215+ stl: STLLoader ,
216+ fbx: FBXLoader ,
217+ obj: OBJLoader
218+ }
219+
220+ const materialParams = {
221+ transparent: true ,
222+ opacity: 0.8 ,
223+ color: 0xd7d7d7 ,
224+ flatShading: true
225+ }
226+
227+ const lightParams = {
228+ color: 0xffffff ,
229+ intensity: 1000 ,
230+ posX: 2.5 ,
231+ posY: 15 ,
232+ posZ: 25 ,
233+ ambient: true
234+ }
235+
236+ async function renderModel(extension : string ) {
237+ const ModelLoader = LoaderMap [extension ]
238+
239+ const model = await new ModelLoader ().loadAsync (unref (currentUrl ), (xhr ) => {
202240 const downloaded = Math .floor ((xhr .loaded / xhr .total ) * 100 )
203241 if (downloaded % 5 === 0 ) {
204242 loadingProgress .value = downloaded
205243 }
206244 })
207245
208- const modelScene = model .scene
209- // model size
210- const box = new Box3 ().setFromObject (modelScene )
246+ debug (model )
247+
248+ const box = new Box3 ()
249+ if (! model .hasOwnProperty (' scene' ) && extension === ' stl' ) {
250+ const mesh = new Mesh (model , defaultMaterial ())
251+ scene .add (mesh )
252+ box .setFromBufferAttribute (model .attributes .position )
253+ } else if (! model .hasOwnProperty (' scene' ) && (extension === ' fbx' || extension === ' obj' )) {
254+ box .setFromObject (model )
255+ model .traverse (function (child ) {
256+ if (child .isMesh ) {
257+ child .material = defaultMaterial ()
258+ }
259+ })
260+ scene .add (model )
261+ } else {
262+ box .setFromObject (model .scene )
263+ }
264+
211265 iniCamPosition = box .getCenter (new Vector3 ())
212266
213267 // direct camera at model
@@ -216,15 +270,36 @@ async function renderModel() {
216270 camera .position .z = iniCamZPosition
217271 camera .lookAt (iniCamPosition )
218272
219- // center model
220- modelScene .position .sub (iniCamPosition )
221- scene .add (modelScene )
222-
223273 loadingModel .value = false
224- currentModel .value = modelScene
274+ if (extension === ' glb' ) {
275+ const modelScene = model .scene
276+ // center model
277+ modelScene .position .sub (iniCamPosition )
278+ scene .add (modelScene )
279+ currentModel .value = modelScene
280+ } else {
281+ currentModel .value = scene
282+ }
283+
225284 unref (sceneWrapper ).appendChild (renderer .domElement )
226285 render (Date .now ())
227286}
287+
288+ function loadLights(): void {
289+ const light = new PointLight (lightParams .color , lightParams .intensity )
290+ light .position .set (lightParams .posX , lightParams .posY , lightParams .posZ )
291+ scene .add (light )
292+
293+ if (lightParams .ambient ) {
294+ const ambientLight = new AmbientLight ()
295+ scene .add (ambientLight )
296+ }
297+ }
298+
299+ function defaultMaterial(): MeshPhongMaterial {
300+ return new MeshPhongMaterial (materialParams )
301+ }
302+
228303function render(animStartTime : number ) {
229304 animationId .value = requestAnimationFrame (() => render (animStartTime ))
230305 // TODO: enable animation
@@ -236,29 +311,29 @@ function render(animStartTime: number) {
236311 controls .update ()
237312 renderer .render (scene , camera )
238313}
314+
239315async function renderNewModel() {
240316 cancelAnimationFrame (unref (animationId ))
241- scene .remove (scene .getObjectByName (unref (currentModel ).name ))
242317
243318 await updateUrl ()
244319
245320 loadingModel .value = true
246321 hasError .value = false
247- await renderModel ()
322+ await renderModel (unref ( fileType ) )
248323}
324+
249325function cleanup3dScene() {
250- scene .traverse ((obj ) => {
251- scene .remove (obj )
252- })
253326 cancelAnimationFrame (unref (animationId ))
254327 renderer .dispose ()
255328}
329+
256330function changeCursor(state : string ) {
257331 const el = unref (sceneWrapper )
258332 if (el .classList .contains (' model-viewport' )) {
259333 el .style .cursor = state
260334 }
261335}
336+
262337async function setActiveModel(driveAliasAndItem : string ) {
263338 for (let i = 0 ; i < unref (modelFiles ).length ; i ++ ) {
264339 if (
@@ -271,6 +346,7 @@ async function setActiveModel(driveAliasAndItem: string) {
271346 }
272347 }
273348}
349+
274350function updateLocalHistory() {
275351 if (! unref (currentFileContext )) {
276352 return
@@ -286,38 +362,54 @@ function updateLocalHistory() {
286362 query: { ... unref (route ).query , ... query }
287363 })
288364}
365+
289366async function next() {
290367 if (! unref (isModelReady )) {
291368 return
292369 }
370+
293371 if (unref (activeIndex ) + 1 >= unref (modelFiles ).length ) {
294372 activeIndex .value = 0
295373 } else {
296374 activeIndex .value ++
297375 }
298376
299377 updateLocalHistory ()
378+ await unloadModels ()
300379 // TODO: how to prevent activeFiles from being reduced
301380 // load activeFiles
302381 await loadFolderForFileContext (unref (currentFileContext ))
303382 await renderNewModel ()
304383}
384+
305385async function prev() {
306386 if (! unref (isModelReady )) {
307387 return
308388 }
389+
309390 if (unref (activeIndex ) === 0 ) {
310391 activeIndex .value = unref (modelFiles ).length - 1
311392 } else {
312393 activeIndex .value --
313394 }
314395
315396 updateLocalHistory ()
397+ await unloadModels ()
316398 // TODO: how to prevent activeFiles from being reduced
317399 // load activeFiles
318400 await loadFolderForFileContext (unref (currentFileContext ))
319401 await renderNewModel ()
320402}
403+
404+ async function unloadModels(): Promise <void > {
405+ for (let i = scene .children .length - 1 ; i >= 0 ; i -- ) {
406+ let obj = scene .children [i ]
407+ if (unref (obj .type ) === ' Group' || unref (obj .type ) === ' Mesh' ) {
408+ scene .remove (obj )
409+ }
410+ }
411+ }
412+
321413function toggleFullscreenMode() {
322414 const activateFullscreen = ! unref (isFullScreenModeActivated )
323415 const el = unref (sceneWrapper )
@@ -332,6 +424,7 @@ function toggleFullscreenMode() {
332424 }
333425 }
334426}
427+
335428function resetModelPosition() {
336429 if (unref (isModelReady )) {
337430 camera .position .copy (iniCamPosition )
@@ -340,6 +433,15 @@ function resetModelPosition() {
340433 camera .lookAt (iniCamPosition )
341434 }
342435}
436+
437+ function debug(output ) {
438+ if (debugIsEnabled ) {
439+ scene .add (new AxesHelper (10 ))
440+ console .log (' ####### DEBUG 3D MODEL #######' )
441+ console .log (output )
442+ console .log (' #####################' )
443+ }
444+ }
343445 </script >
344446
345447<style lang="scss" scoped>
@@ -352,6 +454,7 @@ function resetModelPosition() {
352454 cursor : grab ;
353455 }
354456}
457+
355458#spinner {
356459 & > div {
357460 width : unset ;
0 commit comments