745 lines
25 KiB
JavaScript
745 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.DFG_ASSETS || core.CONFIG?.baseModulePath || '/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() {
|
|
let basePath = sanitizeModuleAssetBasePath(core.DFG_ASSETS || core.CONFIG?.baseModulePath);
|
|
|
|
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';
|
|
}
|
|
|
|
// Override for localhost
|
|
if (core.isLocalPreview) {
|
|
basePath = '/assets';
|
|
}
|
|
|
|
basePath = sanitizeModuleAssetBasePath(basePath);
|
|
|
|
console.log('[loaders] resolved ModuleAssetBasePath:', basePath);
|
|
core.CONFIG.baseModulePath = basePath; // Cache for future use
|
|
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);
|
|
}
|