dfg_3dviewer_js_library/viewer/loaders.js

741 lines
25 KiB
JavaScript

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);
}