Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions core/frontend/src/components/vehiclesetup/OrientationPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,18 @@ import { get_board_model } from '@/components/vehiclesetup/viewers/modelHelper'
import mavlink2rest from '@/libs/MAVLink2Rest'
import autopilot_data from '@/store/autopilot'
import Parameter, { printParam } from '@/types/autopilot/parameter'
import dracoDecoderPath from '@/utils/draco'

// Import DRACO decoder files using Vite's glob import
const dracoFiles = import.meta.glob('/node_modules/three/examples/jsm/libs/draco/*', { eager: true, as: 'url' })
function makeGLTFLoader(): GLTFLoader {
const dracoLoader = new DRACOLoader()
const decoderPath = dracoDecoderPath()
if (decoderPath !== undefined) {
dracoLoader.setDecoderPath(decoderPath)
}
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
return loader
}

class Rotation {
name: string
Expand Down Expand Up @@ -331,15 +340,7 @@ export default {
this.vehicle_obj = undefined
}

const dracoLoader = new DRACOLoader()
// Get the base path from the imported DRACO files
const dracoWasmFile = Object.keys(dracoFiles).find((key) => key.includes('draco_decoder.wasm'))
if (dracoWasmFile) {
const basePath = dracoFiles[dracoWasmFile].replace(/[^/]*$/, '')
dracoLoader.setDecoderPath(basePath)
}
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
const loader = makeGLTFLoader()
loader.load(
this.vehicle_model,
(gltf: GLTF) => {
Expand Down Expand Up @@ -374,15 +375,7 @@ export default {
async add_board_model() {
if (this.scene) {
try {
const dracoLoader = new DRACOLoader()
// Get the base path from the imported DRACO files
const dracoWasmFile = Object.keys(dracoFiles).find((key) => key.includes('draco_decoder.wasm'))
if (dracoWasmFile) {
const basePath = dracoFiles[dracoWasmFile].replace(/[^/]*$/, '')
dracoLoader.setDecoderPath(basePath)
}
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
const loader = makeGLTFLoader()

const board_model = await get_board_model(this.componentModel)

Expand Down
20 changes: 20 additions & 0 deletions core/frontend/src/utils/draco.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// three ships the same Draco decoder that GLTFLoader and <model-viewer> would otherwise
// fetch from a Google CDN, so we bundle it to keep 3D models loadable without internet access.
const decoderFiles = import.meta.glob(
'/node_modules/three/examples/jsm/libs/draco/{draco_decoder.js,draco_decoder.wasm,draco_wasm_wrapper.js}',
{ eager: true, as: 'url' },
)

/**
* URL of the directory serving the bundled Draco decoder, as expected by DRACOLoader's
* decoder path. Returns undefined if the decoder was not bundled, in which case loaders
* keep their default CDN location.
*/
export default function dracoDecoderPath(): string | undefined {
const wasmFile = Object.keys(decoderFiles).find((key) => key.endsWith('draco_decoder.wasm'))
if (wasmFile === undefined) {
console.warn('Bundled Draco decoder not found, 3D models will only load with internet access.')
return undefined
}
return decoderFiles[wasmFile].replace(/[^/]*$/, '')
}
30 changes: 24 additions & 6 deletions core/frontend/src/utils/model_viewer_support.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { ModelViewerGlobalConfig } from '@google/model-viewer/lib/features/loading'

import dracoDecoderPath from '@/utils/draco'

let cachedSupport: boolean | undefined
let loadPromise: Promise<boolean> | undefined

Expand All @@ -24,15 +28,29 @@ export function canUseModelViewer(): boolean {
return cachedSupport
}

// model-viewer reads this global when its first element is created, so it has to be set
// before the library is imported.
function configureDecoderLocation(): void {
const decoderPath = dracoDecoderPath()
if (decoderPath === undefined) {
return
}
const scope = globalThis as unknown as { ModelViewerElement?: ModelViewerGlobalConfig }
scope.ModelViewerElement = { ...scope.ModelViewerElement, dracoDecoderLocation: decoderPath }
}

export function ensureModelViewer(): Promise<boolean> {
if (!canUseModelViewer()) {
return Promise.resolve(false)
}
loadPromise ??= import('@google/model-viewer/dist/model-viewer')
.then(() => true)
.catch((error) => {
console.warn('Failed to load model-viewer, proceeding without 3D viewer.', error)
return false
})
if (loadPromise === undefined) {
configureDecoderLocation()
loadPromise = import('@google/model-viewer/dist/model-viewer')
.then(() => true)
.catch((error) => {
console.warn('Failed to load model-viewer, proceeding without 3D viewer.', error)
return false
})
}
return loadPromise
}
6 changes: 5 additions & 1 deletion core/frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ export default defineConfig(({ command, mode }) => {
// List of file patterns to excluded from hashing
const noHashPatterns = [
/ArduPilot-Parameter-Repository.*\.json$/,
/three\/examples\/jsm\/libs\/draco\//
];

if (assetInfo.name) {
// DRACOLoader is configured with a directory and looks up the decoder files by
// name inside it, so they must be unhashed and kept together.
if (/three\/examples\/jsm\/libs\/draco\//.test(assetInfo.name)) {
return `assets/draco/[name][extname]`;
}
if (noHashPatterns.some(pattern => pattern.test(assetInfo.name))) {
return `assets/[name][extname]`;
}
Expand Down
Loading