import THREE from "./init.js"; export const loadDDSLoader = async () => (await import("three/examples/jsm/loaders/DDSLoader.js")).DDSLoader; export const loadMTLLoader = async () => (await import("three/examples/jsm/loaders/MTLLoader.js")).MTLLoader; export const loadOBJLoader = async () => (await import("three/examples/jsm/loaders/OBJLoader.js")).OBJLoader; export const loadFBXLoader = async () => (await import("three/examples/jsm/loaders/FBXLoader.js")).FBXLoader; export const loadPLYLoader = async () => (await import("three/examples/jsm/loaders/PLYLoader.js")).PLYLoader; export const loadColladaLoader = async () => (await import("three/examples/jsm/loaders/ColladaLoader.js")).ColladaLoader; export const loadSTLLoader = async () => (await import("three/examples/jsm/loaders/STLLoader.js")).STLLoader; export const loadXYZLoader = async () => (await import("three/examples/jsm/loaders/XYZLoader.js")).XYZLoader; export const loadTDSLoader = async () => (await import("three/examples/jsm/loaders/TDSLoader.js")).TDSLoader; export const loadPCDLoader = async () => (await import("three/examples/jsm/loaders/PCDLoader.js")).PCDLoader; export const loadGLTFLoader = async () => (await import("three/examples/jsm/loaders/GLTFLoader.js")).GLTFLoader; export const loadDRACOLoader = async () => (await import("three/examples/jsm/loaders/DRACOLoader.js")).DRACOLoader; export const loadIFCLoader = async () => (await import("./js/loaders/IFCLoader.js")).IFCLoader; export const loadRoomEnvironment = async () => (await import("three/examples/jsm/environments/RoomEnvironment.js")).RoomEnvironment; export const loadHDRLoader = async () => (await import("three/examples/jsm/loaders/HDRLoader.js")).HDRLoader; import { core } from './core.js'; import { fetchSettings, presentationMode } from "./metadata.js"; import { reportViewerError, showToast, toastHelper } from "./viewer-utils.js"; export var outlineClipping; let environmentTextureCache = {}; const loaderMap = { gltf: loadGLTFLoader, glb: loadGLTFLoader, obj: loadOBJLoader, fbx: loadFBXLoader, ply: loadPLYLoader, stl: loadSTLLoader, dae: loadColladaLoader, xyz: loadXYZLoader, '3ds': loadTDSLoader, pcd: loadPCDLoader, ifc: loadIFCLoader }; async function createLoader(ext) { const loadLoader = loaderMap[ext]; if (!loadLoader) { throw new Error(`Unsupported format: ${ext}`); } const LoaderClass = await loadLoader(); return new LoaderClass(); } const ENV_BUILD = __BUILD__; const MODULES_PATH = __MODULES_PATH__; const ENV_SUBDIR = __ENV_SUBDIR__; console.log('[loaders] ENV_BUILD:', ENV_BUILD); console.log('[loaders] MODULES_PATH:', MODULES_PATH); console.log('[loaders] ENV_SUBDIR:', ENV_SUBDIR); function normalizeWasmPath(path) { if (typeof window === 'undefined' || !path) return path; let normalized = path.trim(); // Force secure scheme for explicit http resources if (normalized.startsWith('http://')) { normalized = 'https://' + normalized.slice('http://'.length); } else if (normalized.startsWith('//')) { normalized = `${window.location.protocol}${normalized}`; } else if (normalized.startsWith('/')) { normalized = `${window.location.protocol}//${window.location.host}${normalized}`; } else if (!/^[a-zA-Z][\w+-.]*:/.test(normalized)) { normalized = new URL(normalized, window.location.href).href; } // Normalize duplicate slashes while keeping protocol separator intact try { const url = new URL(normalized); url.pathname = url.pathname.replace(/\/\/{2,}/g, '/'); normalized = url.href; } catch (err) { normalized = normalized.replace(/\/\/{2,}/g, '/'); } return normalized; } function sanitizeModuleAssetBasePath(input) { if (!input || typeof input !== 'string') { return ''; } let basePath = input.trim().replace(/\/$/, ''); if (!basePath) { return ''; } if (/^[a-zA-Z][\w+-.]*:\/\//.test(basePath)) { try { const url = new URL(basePath); const hostSegment = `/${url.host}`; if (url.pathname.startsWith(hostSegment)) { url.pathname = url.pathname.slice(hostSegment.length) || '/'; } url.pathname = url.pathname.replace(/\/\/{2,}/g, '/'); return url.href.replace(/\/$/, ''); } catch (_err) { return basePath; } } if (/^[^\/]+\.[^\/]+\/.+/.test(basePath)) { const parts = basePath.split('/'); const host = parts.shift(); const remainder = `/${parts.join('/')}`.replace(/\/\/{2,}/g, '/'); if ( host && typeof window !== 'undefined' && (host === window.location.host || /^\w[\w.-]*\.[a-z]{2,}$/i.test(host)) ) { return remainder.replace(/\/$/, ''); } } if (/^\/[^\/]+\.[^\/]+\/.+/.test(basePath)) { const parts = basePath.split('/').filter(Boolean); const host = parts.shift(); const remainder = `/${parts.join('/')}`.replace(/\/\/{2,}/g, '/'); if ( host && typeof window !== 'undefined' && (host === window.location.host || /^\w[\w.-]*\.[a-z]{2,}$/i.test(host)) ) { return remainder.replace(/\/$/, ''); } } return basePath.replace(/\/\/{2,}/g, '/'); } function prepareOutlineClipping(_object) { core.outlineClipping = _object.clone(true); var gutsMaterial = new THREE.MeshBasicMaterial({ color: "crimson", side: THREE.BackSide, clippingPlanes: core.clippingPlanes, clipShadows: true, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1, }); core.outlineClipping.traverse(function (child) { if (child.type == "Mesh" || child.type == "Object3D") { child.material = gutsMaterial; } }); core.outlineClipping.visible = false; return core.outlineClipping; } function setupSingleMaterial(materials, material) { if (material.map) { material.map.anisotropy = 16; material.map.colorSpace = THREE.SRGBColorSpace; } material.envMapIntensity = 0.76; material.roughness = Math.min(material.roughness * 1.35, 1); material.clipShadows = true; material.side = core.PRESENTATION_MODE ? THREE.DoubleSide : THREE.FrontSide; material.clippingPlanes = core.PRESENTATION_MODE ? [] : core.clippingPlanes; //material.clipIntersection = false; material.onBeforeCompile = (shader) => { shader.fragmentShader = shader.fragmentShader.replace( 'reflectedLight.directSpecular += directSpecular;', ` reflectedLight.directSpecular += directSpecular * 0.15; ` ); }; material.needsUpdate = true; if (material.name === "") material.name = material.uuid; var newMaterial = { name: material.name, uuid: material.uuid }; if (!materials.some((item) => item.uuid === newMaterial.uuid)) materials.push(newMaterial); } function setupMaterials(_object) { var materials = []; if (_object.isMesh) { _object.castShadow = true; _object.receiveShadow = true; if ( _object.geometry && typeof _object.geometry.computeVertexNormals === "function" && !_object.geometry.getAttribute?.("normal") ) { _object.geometry.computeVertexNormals(); } if (_object.material.isMaterial) { setupSingleMaterial(materials, _object.material); } else if (Array.isArray(_object.material)) { _object.material.forEach((material) => setupSingleMaterial(materials, material) ); } } return materials; } function getMaterialByID(_object, _uuid) { var _material; _object.traverse(function (child) { if ( child.isMesh && child.material.isMaterial && child.material.uuid === _uuid ) { _material = child.material; } }); return _material; } function traverseMesh(object) { setupMaterials(object); object.traverse(function (child) { setupMaterials(child); }); if (window.Viewer?.initializeMaterialsEditor && core.PRESENTATION_MODE !== true) { window.Viewer.initializeMaterialsEditor(object); } } function getEnvironmentTextureForPreset(renderer, preset = "neutral") { if (!renderer) return Promise.resolve(null); core.scene.environmentIntensity = core.environmentMapIntensity || 0.5; // Initialize cache for this preset if not exists if (!environmentTextureCache[preset]) { environmentTextureCache[preset] = (async () => { const pmrem = new THREE.PMREMGenerator(renderer); try { if (preset === "studio") { // Studio uses RoomEnvironment const TempRoomEnvironment = await loadRoomEnvironment(); return pmrem.fromScene(new TempRoomEnvironment()).texture; } else if (preset === "neutral" || preset === "sunny" || preset === "goldenHour") { // Load HDR map for other presets const HDRLoader = await loadHDRLoader(); const loader = new HDRLoader(); const baseModulePath = core.CONFIG?.baseModulePath || core.DFG_ASSETS || '/assets'; const mapFilename = preset === "goldenHour" ? "golden_hour.hdr" : `${preset}.hdr`; const mapUrl = `${baseModulePath.replace(/\/$/, '')}/maps/${mapFilename}`; const texture = await new Promise((resolve, reject) => { loader.load(mapUrl, resolve, undefined, reject); }); texture.mapping = THREE.EquirectangularReflectionMapping; return pmrem.fromEquirectangular(texture).texture; } return null; } finally { pmrem.dispose(); } })(); } return environmentTextureCache[preset]; } function getEnvironmentTexture(renderer) { // Legacy function for backwards compatibility return getEnvironmentTextureForPreset(renderer, "neutral"); } function markEnvironmentMaterialsDirty(root) { root?.traverse?.((child) => { const materials = child?.material ? (Array.isArray(child.material) ? child.material : [child.material]) : []; materials.forEach((material) => { if (material?.isMeshStandardMaterial || material?.isMeshPhysicalMaterial) { material.needsUpdate = true; } }); }); } export async function syncSceneEnvironment(enabled = true, preset = null) { if (!core.scene) return; // Use provided preset or fall back to viewer's preset, then neutral const effectivePreset = preset || window.viewer?.environmentMapPreset || "neutral"; if (enabled) { core.scene.environment = await getEnvironmentTextureForPreset(core.renderer, effectivePreset); if (core.scene.environmentIntensity === 0) { core.scene.environmentIntensity = 0.5; } } else { core.scene.environment = null; core.scene.environmentIntensity = 0; } markEnvironmentMaterialsDirty(core.scene); } function reportLoadError(error, context = "") { const message = reportViewerError(error, { context, consoleLabel: "Viewer load error:", }); core.circle?.complete?.(1200); if (typeof core.EXIT_CODE !== "undefined") core.EXIT_CODE = 1; return message; } export async function loadModel() { core.loadingLog?.start?.(); let modelPath = core.fileObject.filename.startsWith('blob:') ? core.fileObject.filename : core.fileObject.path + core.fileObject.filename; if (core.CONFIG.entity.proxyPath !== undefined && !core.fileObject.filename.startsWith('blob:')) { modelPath = core.getProxyPath(modelPath, core.CONFIG, core.fileObject); } function loadAsync(loader, url, progressHandler = onProgress) { return new Promise((resolve, reject) => { loader.load(url, resolve, progressHandler, reject); }); } function updateLoadingStage(stageKey, progressValue = null) { core.circle?.setStage?.(stageKey, progressValue); core.loadingLog?.setStage?.(stageKey, progressValue); } async function afterLoad({ object }) { if (object === null || typeof object === "undefined") { throw new Error("Loaded object is null or undefined."); } updateLoadingStage("loadingLog.loadingTextures", 99); // Keep authoring transforms in presentation mode to avoid collapsing model parts. if (!core.PRESENTATION_MODE) { // Reset transform to ensure consistent positioning if (Array.isArray(object)) { object.forEach(obj => { obj.position.set(0, 0, 0); obj.rotation.set(0, 0, 0); obj.scale.set(1, 1, 1); obj.updateMatrixWorld(true); }); } else { object.position.set(0, 0, 0); object.rotation.set(0, 0, 0); object.scale.set(1, 1, 1); object.updateMatrixWorld(true); } core.handHint.hidden = true; } window.viewer.modelLoaded = true; updateLoadingStage("loadingLog.preparingGeometry", 99); traverseMesh(object); if (!core.PRESENTATION_MODE) { const isArchiveDerivedPath = /\/[^/]+_(ZIP|RAR|TAR|XZ|GZ)\/gltf\/$/i.test(core.fileObject.path); if (!isArchiveDerivedPath) { if (core.fileObject.extension.toLowerCase() === "gltf" || core.fileObject.extension.toLowerCase() === "glb") { core.fileObject.path = core.fileObject.path.replace("/gltf/", "/"); } else { core.fileObject.path = core.fileObject.path.replace("gltf/", ""); } } updateLoadingStage("loadingLog.fetchingMetadata", 99); await fetchSettings(object); updateLoadingStage("loadingLog.settingUpMaterials", 99); core.outlineClipping = prepareOutlineClipping(object); if (Array.isArray(object)) { core.helperObjects.push(object[0]); } else { core.helperObjects.push(object); } core.scene.add(core.outlineClipping); } else { updateLoadingStage("loadingLog.settingUpMaterials", 99); presentationMode(object, null).catch(error => { reportLoadError(error, "Presentation mode setup failed"); showToast("toasts.presentationModeError", "error"); }); } updateLoadingStage("loadingLog.settingUpLighting", 99); if (Array.isArray(object)) { object.forEach(o => core.scene.add(o)); } else { core.scene.add(object); } core.mainObject.push(object); updateLoadingStage("loadingLog.compilingShaders", 99); await syncSceneEnvironment(core.environmentMapEnabled !== false); } async function loadOBJWithMTL() { const DDSLoader = await loadDDSLoader(); const MTLLoader = await loadMTLLoader(); const OBJLoader = await loadOBJLoader(); const manager = new THREE.LoadingManager(); manager.onLoad = () => toastHelper("objLoaded", "success"); manager.addHandler(/\.dds$/i, new DDSLoader()); const basename = core.fileObject.filename.replace(/\.[^/.]+$/, ""); const filename = core.fileObject.filename; if (!core.CONFIG.noMTL) { try { const materials = await new Promise((resolve, reject) => { new MTLLoader(manager) .setPath(core.fileObject.path) .load(basename + ".mtl", resolve, undefined, reject); }); materials.preload(); const obj = await new Promise((resolve, reject) => { new OBJLoader(manager) .setMaterials(materials) .setPath(core.fileObject.path) .load(filename, resolve, onProgress, reject); }); obj.position.set(0, 0, 0); return obj; } catch (error) { core.CONFIG.noMTL = true; toastHelper("mtlLoadError", "error"); console.warn("MTL load failed, falling back to OBJ-only load.", error); } } const obj = await new Promise((resolve, reject) => { new OBJLoader() .setPath(core.fileObject.path) .load(filename, resolve, onProgress, reject); }); obj.position.set(0, 0, 0); return obj; } function normalizePath(path) { if (!path || typeof path !== 'string') { return path; } if (/^[a-zA-Z][\w+-.]*:\/\//.test(path)) { try { const url = new URL(path); url.pathname = url.pathname.replace(/\/{2,}/g, '/'); return url.href; } catch (_err) { return path; } } return path.replace(/\/{2,}/g, '/'); } async function resolveIfcWasmPath(basePath) { const candidates = [ normalizePath(basePath.replace(/\/$/, '') + '/ifc/'), normalizePath(basePath.replace(/\/$/, '') + '/ifc'), ]; for (const candidate of candidates) { const wasmUrl = candidate.replace(/\/$/, '') + '/web-ifc.wasm'; try { const res = await fetch(wasmUrl, { method: 'HEAD', cache: 'no-store' }); if (res.ok) { return candidate; } } catch (err) { // ignored, try next candidate } } return null; } async function loadGLTFModel() { let gltfModelPath = core.fileObject.filename.startsWith('blob:') ? core.fileObject.filename : core.fileObject.path + core.fileObject.basename + "." + core.fileObject.extension; if (core.CONFIG.entity.proxyPath !== undefined && !core.fileObject.filename.startsWith('blob:')) { gltfModelPath = core.getProxyPath(gltfModelPath); } const dracoBase = normalizePath(normalizeWasmPath(`${getModuleAssetBasePath()}/draco/gltf/`)); const loader = await createLoader(core.fileObject.extension.toLowerCase()); const DRACOLoader = await loadDRACOLoader(); const draco = new DRACOLoader(); if (ENV_BUILD !== 'test' && ENV_BUILD !== 'dev') { draco.setDecoderConfig({ type: 'js' }); } draco.setDecoderPath(dracoBase); loader.setDRACOLoader(draco); try { const gltf = await new Promise((resolve, reject) => { loader.load( gltfModelPath, resolve, (xhr) => { progressLoaderHandler(xhr); }, reject ); }); return gltf.scene; } finally { draco.dispose(); } } try { switch (core.fileObject.extension.toLowerCase()) { case "obj": { const object = await loadOBJWithMTL(); await afterLoad({ object }); break; } case "fbx": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const object = await loadAsync(loader, modelPath, onProgress); object.position.set(0, 0, 0); await afterLoad({ object }); break; } case "ply": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const geometry = await loadAsync(loader, modelPath, onProgress); if (!geometry.getAttribute?.("normal")) { geometry.computeVertexNormals(); } const material = new THREE.MeshStandardMaterial({ color: 0x0055ff, flatShading: true }); const object = new THREE.Mesh(geometry, material); object.position.set(0, 0, 0); object.castShadow = true; object.receiveShadow = true; await afterLoad({ object }); break; } case "dae": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const collada = await loadAsync(loader, modelPath, onProgress); const object = collada.scene; object.position.set(0, 0, 0); await afterLoad({ object }); break; } case "ifc": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const basePath = getModuleAssetBasePath(); let ifcWasmPath = await resolveIfcWasmPath(basePath); if (!ifcWasmPath) { const errorMsg = `[loadModel] IFC WASM not found in ${basePath}/ifc; please verify path and permissions`; console.error(errorMsg); throw new Error(errorMsg); } const normalizedIfcWasmPath = normalizeWasmPath(ifcWasmPath); console.log('[loadModel] IFC WASM path:', normalizedIfcWasmPath); loader.ifcManager.setWasmPath(normalizedIfcWasmPath, true); const object = await loadAsync(loader, modelPath, onProgress); await afterLoad({ object }); break; } case "stl": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const geometry = await loadAsync(loader, modelPath, onProgress); let meshMaterial = new THREE.MeshPhongMaterial({ color: 0xff5533, specular: 0x111111, shininess: 200 }); if (geometry.hasColors) { meshMaterial = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true }); } const object = new THREE.Mesh(geometry, meshMaterial); object.position.set(0, 0, 0); object.castShadow = true; object.receiveShadow = true; await afterLoad({ object }); break; } case "xyz": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const geometry = await loadAsync(loader, modelPath, onProgress); geometry.center(); const material = new THREE.PointsMaterial({ size: 0.1, vertexColors: geometry.hasAttribute("color") === true }); const object = new THREE.Points(geometry, material); object.position.set(0, 0, 0); await afterLoad({ object }); break; } case "pcd": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); const mesh = await loadAsync(loader, modelPath, onProgress); mesh.geometry?.center?.(); if (mesh.material) { mesh.material.size = Math.max(mesh.material.size ?? 0, 0.1); } await afterLoad({ object: mesh }); break; } case "json": { const loader = new THREE.ObjectLoader(); const object = await loadAsync(loader, modelPath, onProgress); object.position.set(0, 0, 0); await afterLoad({ object }); break; } case "3ds": { const loader = await createLoader(core.fileObject.extension.toLowerCase()); loader.setResourcePath(core.fileObject.path); let mp = core.fileObject.path; if (core.CONFIG.entity.proxyPath !== undefined) mp = core.getProxyPath(mp); const object = await loadAsync(loader, mp + core.fileObject.basename + "." + core.fileObject.extension, onProgress); await afterLoad({ object }); break; } case "glb": case "gltf": { const object = await loadGLTFModel(); await afterLoad({ object }); break; } default: toastHelper("unsupportedExtension", "warning"); core.loadingLog?.fail?.(); return; } updateLoadingStage("loadingLog.modelLoaded", 100); core.circle?.complete?.(2600); core.editorToolbar?.classList.remove('editorToolbar-hidden'); core.editorToolbar?.classList.add('editorToolbar-visible'); core.loadingLog?.finish?.(); if (!core.PRESENTATION_MODE) { toastHelper("modelLoaded", "success", { filename: core.fileObject.filename }); } else { toastHelper("presentationModeReady", "success"); } if (typeof core.EXIT_CODE !== "undefined") core.EXIT_CODE = 0; core.UltraLoader?.finish(); core.poller?.updateSteps(2); } catch (error) { core.loadingLog?.fail?.(); reportLoadError(error, `Failed to load ${core.fileObject.filename}`); throw error; } } export const getModuleAssetBasePath = function() { const configuredPath = sanitizeModuleAssetBasePath(core.CONFIG?.baseModulePath); let basePath = configuredPath || sanitizeModuleAssetBasePath(core.DFG_ASSETS); if (!basePath && typeof import.meta !== 'undefined' && import.meta.url) { const moduleUrl = new URL(import.meta.url); basePath = moduleUrl.pathname.includes('/assets/') ? new URL('../assets/', moduleUrl).pathname.replace(/\/$/, '') : new URL('./assets/', moduleUrl).pathname.replace(/\/$/, ''); } if (!basePath) { basePath = '/assets'; } // Standalone local dev servers expose assets at /assets; embedded hosts provide baseModulePath. if (core.isLocalPreview && !configuredPath) { basePath = '/assets'; } basePath = sanitizeModuleAssetBasePath(basePath); console.log('[loaders] resolved ModuleAssetBasePath:', basePath); core.CONFIG.baseModulePath = basePath; core.DFG_ASSETS = basePath; return basePath; }; export const onError = function (_event) { reportLoadError(_event, "Loader error"); }; export const onErrorMTL = async function (_event) { core.CONFIG.noMTL = true; toastHelper("mtlLoadError", "error"); await loadModel(); }; export const onErrorGLB = async function (_event, params, loadedTimes) { console.log("Loader error: " + _event); if (window.__E2E__ && window.viewer) { window.viewer.errors ??= []; window.viewer.errors.push(String(_event)); } core.loadedFile = params.path + params.basename + core.loadedFile + "gltf/"; if (typeof _event !== undefined && loadedTimes <= 1 && window.viewer.modelLoaded === false) { await loadModel(); loadedTimes++; } else { toastHelper("glbLoadError", "error"); } }; export const onProgress = function (xhr) { progressLoaderHandler(xhr); }; const progressLoaderHandler = function (xhr) { if (!core.circle) return; const total = xhr.total || xhr.loaded || 1; const percentComplete = Math.min((xhr.loaded / total) * 100, 99); if (!Number.isFinite(percentComplete)) return; core.circle.show(); core.circle.set(percentComplete, 100); core.editorToolbar?.classList.remove('editorToolbar-hidden'); core.editorToolbar?.classList.add('editorToolbar-visible'); core.loadingLog?.update?.(percentComplete); core.UltraLoader?.set(percentComplete); }