import { Mesh } from '../../scene/mesh.js';
import { Points } from '../../scene/points.js';
import { Line } from '../../scene/line.js';
import { PointsMaterial } from '../../material/points-material.js';
import { SolidColorMaterial } from '../../material/solid-color-material.js';
import { ObjGeometryBuilder } from './obj-geometry-builder.js';
import { ObjMaterialFactory } from './obj-material-factory.js';
import { ObjParser } from './obj-parser.js';
import { MtlParser } from './mtl-parser.js';
import { MtlTextureCache } from './mtl-texture-cache.js';
/**
* Default texture unit index.
*
* @type {number}
*/
const DEFAULT_TEXTURE_UNIT_INDEX = 0;
/**
* Default diffuse color for materials (white).
*
* @type {Float32Array}
*/
const DEFAULT_DIFFUSE_COLOR = new Float32Array([1.0, 1.0, 1.0]);
/**
* Default opacity for materials.
*
* @type {number}
*/
const DEFAULT_OPACITY = 1.0;
/**
* Empty string constant.
*
* @type {string}
*/
const EMPTY_STRING = '';
/**
* Default base URL, used for resolving relative assets.
*
* @type {string}
*/
const DEFAULT_BASE_URL = EMPTY_STRING;
/**
* Path separator for URL detection and slicing.
*
* @type {string}
*/
const PATH_SEPARATOR = '/';
/**
* Prefix used for relative paths.
*
* @type {string}
*/
const DOT_SLASH_PREFIX = './';
/**
* Backslash path separator.
*
* @type {string}
*/
const BACKSLASH_SEPARATOR = '\\';
/**
* Matches backslash characters in paths.
*
* @type {RegExp}
*/
const BACKSLASH_REGEX = /\\/gu;
/**
* Matches multiple consecutive slashes in a path.
*
* @type {RegExp}
*/
const MULTIPLE_SLASHES_REGEX = /\/{2,}/gu;
/**
* Regex used to detect absolute URLs.
*
* @type {RegExp}
*/
const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+.-]*:/u;
/**
* Token, used to wrap quoted paths.
*
* @type {string}
*/
const QUOTE_TOKEN = '"';
/**
* Index value used, when a substring search fails.
*
* @type {number}
*/
const NOT_FOUND_INDEX = -1;
/**
* Index used to reference the second element in arrays.
*
* @type {number}
*/
const SECOND_INDEX = 1;
/**
* Offset used, when slicing to a base URL.
*
* @type {number}
*/
const BASE_PATH_SLICE_OFFSET = 1;
/**
* Zero value used for numeric comparisons.
*
* @type {number}
*/
const ZERO_VALUE = 0;
/**
* Number of components for RGB color.
*
* @type {number}
*/
const COLOR_COMPONENT_COUNT = 3;
/**
* Entry type for mesh geometry.
*
* @type {string}
*/
const ENTRY_TYPE_MESH = 'mesh';
/**
* Entry type for point geometry.
*
* @type {string}
*/
const ENTRY_TYPE_POINTS = 'points';
/**
* Entry type for line geometry.
*
* @type {string}
*/
const ENTRY_TYPE_LINE = 'line';
/**
* String literal for typeof checks.
*
* @type {string}
*/
const TYPEOF_STRING = 'string';
/**
* String literal for typeof checks (object).
*
* @type {string}
*/
const TYPEOF_OBJECT = 'object';
/**
* Error message for invalid WebGL context.
*
* @type {string}
*/
const ERROR_WEBGL_CONTEXT_TYPE = '`ObjMtlLoader` expects a `WebGL2RenderingContext`.';
/**
* Error message for invalid options object.
*
* @type {string}
*/
const ERROR_OPTIONS_TYPE = '`ObjMtlLoader` expects options as a plain object.';
/**
* Error message for invalid texture unit index.
*
* @type {string}
*/
const ERROR_TEXTURE_UNIT_INDEX_TYPE = '`ObjMtlLoader` expects `textureUnitIndex` as a non-negative integer.';
/**
* Error message for invalid default color type.
*
* @type {string}
*/
const ERROR_DEFAULT_COLOR_TYPE = '`ObjMtlLoader` expects `defaultColor` as `number[]` or `Float32Array`.';
/**
* Error message for invalid default color length.
*
* @type {string}
*/
const ERROR_DEFAULT_COLOR_LENGTH = '`ObjMtlLoader` expects `defaultColor` to have 3 components.';
/**
* Error message for invalid load options object.
*
* @type {string}
*/
const ERROR_LOAD_OPTIONS_TYPE = '`ObjMtlLoader` expects load options as a plain object.';
/**
* Error message for invalid objUrl.
*
* @type {string}
*/
const ERROR_OBJ_URL_TYPE = '`ObjMtlLoader.loadFromUrls` expects `objUrl` as a string.';
/**
* Error message for invalid mtlUrl.
*
* @type {string}
*/
const ERROR_MTL_URL_TYPE = '`ObjMtlLoader.loadFromUrls` expects `mtlUrl` as a string, when provided.';
/**
* Error message for invalid baseUrl.
*
* @type {string}
*/
const ERROR_BASE_URL_TYPE = '`ObjMtlLoader.loadFromUrls` expects `baseUrl` as a string.';
/**
* Error message for invalid texture base URL.
*
* @type {string}
*/
const ERROR_TEXTURE_BASE_URL_TYPE = '`ObjMtlLoader.loadFromUrls` expects `textureBaseUrl` as a string, when provided.';
/**
* Error message for invalid objFile.
*
* @type {string}
*/
const ERROR_OBJ_FILE_TYPE = '`ObjMtlLoader.loadFromFiles` expects `objFile` as `File`.';
/**
* Error message for invalid mtlFiles.
*
* @type {string}
*/
const ERROR_MTL_FILES_TYPE = '`ObjMtlLoader.loadFromFiles` expects `mtlFiles` as `Map`, when provided.';
/**
* Error message for invalid assetUrlMap.
*
* @type {string}
*/
const ERROR_ASSET_URL_MAP_TYPE = '`ObjMtlLoader.loadFromFiles` expects `assetUrlMap` as `Map`, when provided.';
/**
* Error message for invalid baseUrl in files loader.
*
* @type {string}
*/
const ERROR_FILES_BASE_URL_TYPE = '`ObjMtlLoader.loadFromFiles` expects `baseUrl` as a string.';
/**
* Error message for invalid textureBaseUrl in files loader.
*
* @type {string}
*/
const ERROR_FILES_TEXTURE_BASE_URL_TYPE = '`ObjMtlLoader.loadFromFiles` expects `textureBaseUrl` as a string, when provided.';
/**
* Warning prefix for missing MTL materials.
*
* @type {string}
*/
const WARNING_MTL_MATERIAL_NOT_FOUND_PREFIX = 'MTL material not found for usemtl=';
/**
* Warning suffix prefix for available materials list.
*
* @type {string}
*/
const WARNING_MTL_AVAILABLE_PREFIX = ', available: ';
/**
* Warning list start token for available materials.
*
* @type {string}
*/
const WARNING_MTL_AVAILABLE_START = '[';
/**
* Warning list end token for available materials.
*
* @type {string}
*/
const WARNING_MTL_AVAILABLE_END = ']';
/**
* Warning separator for available material names.
*
* @type {string}
*/
const WARNING_MTL_AVAILABLE_SEPARATOR = ', ';
/**
* Warning prefix for missing diffuse map URL.
*
* @type {string}
*/
const WARNING_MTL_MISSING_DIFFUSE_PREFIX = 'MTL diffuse map URL missing for path=';
/**
* Warning suffix prefix for texture base URL.
*
* @type {string}
*/
const WARNING_MTL_MISSING_DIFFUSE_BASE_PREFIX = ', textureBaseUrl=';
/**
* Warning prefix for failed MTL loads.
*
* @type {string}
*/
const WARNING_MTL_LOAD_FAILED_PREFIX = 'Failed to load MTL: ';
/**
* Warning prefix for failed MTL load reason.
*
* @type {string}
*/
const WARNING_MTL_LOAD_FAILED_REASON_PREFIX = ', reason: ';
/**
* Warning placeholder for unknown MTL load errors.
*
* @type {string}
*/
const WARNING_MTL_LOAD_FAILED_UNKNOWN = 'Unknown error';
/**
* Maximum number of available materials included in warnings.
*
* @type {number}
*/
const WARNING_MTL_AVAILABLE_LIMIT = 5;
/**
* Warning key separator.
*
* @type {string}
*/
const WARNING_KEY_SEPARATOR = '::';
/**
* Error message, when fetch fails.
*
* @type {string}
*/
const ERROR_FETCH_FAILED_PREFIX = 'Failed to fetch resource: ';
/**
* Options used by `ObjMtlLoader`.
*
* @typedef {Object} ObjMtlLoaderOptions
* @property {number} [textureUnitIndex=0] - Texture unit index for textured materials.
* @property {Float32Array | number[]} [defaultColor] - Default diffuse color.
*/
/**
* Options used by `ObjMtlLoader.loadFromUrls`.
*
* @typedef {Object} ObjMtlLoadFromUrlsOptions
* @property {string} objUrl - URL to the OBJ file.
* @property {string} [mtlUrl] - Optional URL to the MTL file.
* @property {string} [baseUrl] - Base URL for resolving relative references.
* @property {string} [textureBaseUrl] - Base URL for resolving texture paths.
*/
/**
* Options used by `ObjMtlLoader.loadFromFiles`.
*
* @typedef {Object} ObjMtlLoadFromFilesOptions
* @property {File} objFile - OBJ file.
* @property {Map<string, File>} [mtlFiles] - Map of MTL files by normalized path/name.
* @property {Map<string, string>} [assetUrlMap] - Map of asset blob URLs by normalized path/name.
* @property {string} [baseUrl] - Base URL for resolving relative references.
* @property {string} [textureBaseUrl] - Base URL for resolving texture paths.
*/
/**
* Result returned by `ObjMtlLoader.loadFromUrls`.
*
* @typedef {Object} ObjMtlLoadResult
* @property {Object3D} root - Root object, containing all meshes.
* @property {Mesh[]} meshes - Loaded meshes, points and lines.
* @property {CustomGeometry[]} geometries - Created geometries.
* @property {Array<LambertMaterial | PhongMaterial | VertexColorMaterial | PointsMaterial | SolidColorMaterial | MtlStandardMaterial>} materials - Created materials.
* @property {Texture2D[]} textures - Textures created by the loader.
*/
/**
* Loader for OBJ/MTL assets.
*/
export class ObjMtlLoader {
/**
* WebGL2 rendering context used to create GPU resources.
*
* @type {WebGL2RenderingContext}
* @private
*/
#webglContext;
/**
* Texture unit index for textured materials.
*
* @type {number}
* @private
*/
#textureUnitIndex;
/**
* Default diffuse color used, when no material info is available.
*
* @type {Float32Array}
* @private
*/
#defaultColor = new Float32Array(DEFAULT_DIFFUSE_COLOR);
/**
* OBJ parser instance.
*
* @type {ObjParser}
* @private
*/
#objParser;
/**
* MTL parser instance.
*
* @type {MtlParser}
* @private
*/
#mtlParser;
/**
* Geometry builder instance.
*
* @type {ObjGeometryBuilder}
* @private
*/
#geometryBuilder;
/**
* Material factory instance.
*
* @type {ObjMaterialFactory}
* @private
*/
#materialFactory;
/**
* Texture cache shared across materials.
*
* @type {MtlTextureCache}
* @private
*/
#textureCache;
/**
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context.
* @param {ObjMtlLoaderOptions} [options] - Loader options.
* @throws {TypeError} When inputs are invalid.
*/
constructor(webglContext, options = {}) {
if (!(webglContext instanceof WebGL2RenderingContext)) {
throw new TypeError(ERROR_WEBGL_CONTEXT_TYPE);
}
if (options === null || typeof options !== TYPEOF_OBJECT || Array.isArray(options)) {
throw new TypeError(ERROR_OPTIONS_TYPE);
}
const {
textureUnitIndex = DEFAULT_TEXTURE_UNIT_INDEX,
defaultColor
} = options;
if (!Number.isInteger(textureUnitIndex) || textureUnitIndex < ZERO_VALUE) {
throw new TypeError(ERROR_TEXTURE_UNIT_INDEX_TYPE);
}
if (defaultColor !== undefined) {
if (!Array.isArray(defaultColor) && !(defaultColor instanceof Float32Array)) {
throw new TypeError(ERROR_DEFAULT_COLOR_TYPE);
}
if (defaultColor.length !== COLOR_COMPONENT_COUNT) {
throw new TypeError(ERROR_DEFAULT_COLOR_LENGTH);
}
this.#defaultColor.set(defaultColor);
}
this.#webglContext = webglContext;
this.#textureUnitIndex = textureUnitIndex;
this.#textureCache = new MtlTextureCache(this.#webglContext);
this.#objParser = new ObjParser();
this.#mtlParser = new MtlParser();
this.#geometryBuilder = new ObjGeometryBuilder(this.#webglContext);
this.#materialFactory = new ObjMaterialFactory(this.#webglContext, {
textureUnitIndex : this.#textureUnitIndex,
defaultColor : this.#defaultColor,
textureCache : this.#textureCache
});
}
/**
* Loads the OBJ/MTL assets from URLs and creates the meshes.
*
* @param {ObjMtlLoadFromUrlsOptions} options - Load options.
* @returns {Promise<ObjMtlLoadResult>}
* @throws {TypeError} When options are invalid.
*/
async loadFromUrls(options = {}) {
if (options === null || typeof options !== TYPEOF_OBJECT || Array.isArray(options)) {
throw new TypeError(ERROR_LOAD_OPTIONS_TYPE);
}
const {
objUrl,
mtlUrl,
baseUrl = DEFAULT_BASE_URL,
textureBaseUrl
} = options;
if (typeof objUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_OBJ_URL_TYPE);
}
if (mtlUrl !== undefined && typeof mtlUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_MTL_URL_TYPE);
}
if (typeof baseUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_BASE_URL_TYPE);
}
if (textureBaseUrl !== undefined && typeof textureBaseUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_TEXTURE_BASE_URL_TYPE);
}
const objText = await ObjMtlLoader.#fetchText(objUrl);
const objData = this.#objParser.parse(objText);
const resolvedBaseUrl = baseUrl || ObjMtlLoader.#getBasePath(objUrl);
const mtlData = new Map();
const mtlBaseUrl = baseUrl || resolvedBaseUrl;
if (mtlUrl) {
const resolvedMtlUrl = ObjMtlLoader.#resolvePath(mtlBaseUrl, mtlUrl);
try {
const mtlText = await ObjMtlLoader.#fetchText(resolvedMtlUrl);
const parsedMtl = this.#mtlParser.parse(mtlText);
for (const [name, material] of parsedMtl.entries()) {
mtlData.set(name, material);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : WARNING_MTL_LOAD_FAILED_UNKNOWN;
console.warn(`${WARNING_MTL_LOAD_FAILED_PREFIX}${resolvedMtlUrl}${WARNING_MTL_LOAD_FAILED_REASON_PREFIX}${errorMessage}`);
}
}
if (!mtlUrl && objData.materialLibraries.length > ZERO_VALUE) {
for (const library of objData.materialLibraries) {
if (!library) {
continue;
}
const resolvedMtlUrl = ObjMtlLoader.#resolvePath(resolvedBaseUrl, library);
try {
const mtlText = await ObjMtlLoader.#fetchText(resolvedMtlUrl);
const parsedMtl = this.#mtlParser.parse(mtlText);
for (const [name, material] of parsedMtl.entries()) {
mtlData.set(name, material);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : WARNING_MTL_LOAD_FAILED_UNKNOWN;
console.warn(`${WARNING_MTL_LOAD_FAILED_PREFIX}${resolvedMtlUrl}${WARNING_MTL_LOAD_FAILED_REASON_PREFIX}${errorMessage}`);
}
}
}
const resolvedTextureBase = textureBaseUrl || resolvedBaseUrl;
return this.#buildMeshes(objData, mtlData, resolvedTextureBase);
}
/**
* Loads OBJ/MTL assets from the local `File` objects.
*
* @param {ObjMtlLoadFromFilesOptions} options - Load options.
* @returns {Promise<ObjMtlLoadResult>} - Promise, that resolves with the created scene root and all created assets.
* @throws {TypeError} When options are invalid.
*/
async loadFromFiles(options = {}) {
if (options === null || typeof options !== TYPEOF_OBJECT || Array.isArray(options)) {
throw new TypeError(ERROR_LOAD_OPTIONS_TYPE);
}
const {
objFile,
mtlFiles = new Map(),
assetUrlMap,
baseUrl = DEFAULT_BASE_URL,
textureBaseUrl
} = options;
if (!(objFile instanceof File)) {
throw new TypeError(ERROR_OBJ_FILE_TYPE);
}
if (!(mtlFiles instanceof Map)) {
throw new TypeError(ERROR_MTL_FILES_TYPE);
}
if (assetUrlMap !== undefined && !(assetUrlMap instanceof Map)) {
throw new TypeError(ERROR_ASSET_URL_MAP_TYPE);
}
if (typeof baseUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_FILES_BASE_URL_TYPE);
}
if (textureBaseUrl !== undefined && typeof textureBaseUrl !== TYPEOF_STRING) {
throw new TypeError(ERROR_FILES_TEXTURE_BASE_URL_TYPE);
}
const objText = await objFile.text();
const objData = this.#objParser.parse(objText);
const resolvedBaseUrl = baseUrl || DEFAULT_BASE_URL;
const mtlData = new Map();
for (const library of objData.materialLibraries) {
const mtlFile = ObjMtlLoader.#getFileFromMap(mtlFiles, library);
if (!mtlFile) {
continue;
}
const mtlText = await mtlFile.text();
const parsedMtl = this.#mtlParser.parse(mtlText);
for (const [name, material] of parsedMtl.entries()) {
mtlData.set(name, material);
}
}
const resolvedTextureBase = textureBaseUrl || resolvedBaseUrl;
return this.#buildMeshes(objData, mtlData, resolvedTextureBase, assetUrlMap);
}
/**
* Fetches text content by URL.
*
* @param {string} url - URL to fetch.
* @returns {Promise<string>} - Promise, that resolves with the response body as text.
* @throws {Error} When the fetch fails.
* @private
*/
static async #fetchText(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(ERROR_FETCH_FAILED_PREFIX + url);
}
return response.text();
}
/**
* Creates meshes for parsed OBJ/MTL data.
*
* @param {Object} objData - Parsed OBJ data.
* @param {Map<string, Object>} mtlData - Parsed MTL data.
* @param {string} textureBaseUrl - Base URL for textures.
* @param {Map<string, string>} [assetUrlMap] - Asset URL override map.
* @returns {Promise<ObjMtlLoadResult>} - Promise, that resolves with the created root object and all created assets.
* @private
*/
async #buildMeshes(objData, mtlData, textureBaseUrl, assetUrlMap) {
const buildResult = this.#geometryBuilder.build(objData);
const root = buildResult.root;
const meshes = [];
const geometries = buildResult.geometries;
const materials = [];
const textures = [];
const missingMaterialWarnings = new Set();
const missingDiffuseWarnings = new Set();
const availableMaterials = Array.from(mtlData.keys());
const availablePreview = availableMaterials.slice(ZERO_VALUE, WARNING_MTL_AVAILABLE_LIMIT);
for (const entry of buildResult.entries) {
const materialDefinition = mtlData.get(entry.materialName) || null;
const textureUrls = ObjMtlLoader.#resolveTextureUrls(materialDefinition, textureBaseUrl, assetUrlMap);
if (!materialDefinition && !missingMaterialWarnings.has(entry.materialName)) {
missingMaterialWarnings.add(entry.materialName);
const availableList = availablePreview.join(WARNING_MTL_AVAILABLE_SEPARATOR);
console.warn(
'%s%s%s%s%s%s',
WARNING_MTL_MATERIAL_NOT_FOUND_PREFIX,
entry.materialName,
WARNING_MTL_AVAILABLE_PREFIX,
WARNING_MTL_AVAILABLE_START,
availableList,
WARNING_MTL_AVAILABLE_END
);
}
if (materialDefinition && materialDefinition.diffuseMap && (!textureUrls || !textureUrls.diffuse)) {
const warnKey = materialDefinition.diffuseMap.path + WARNING_KEY_SEPARATOR + textureBaseUrl;
if (!missingDiffuseWarnings.has(warnKey)) {
missingDiffuseWarnings.add(warnKey);
console.warn(
'%s%s%s%s',
WARNING_MTL_MISSING_DIFFUSE_PREFIX,
materialDefinition.diffuseMap.path,
WARNING_MTL_MISSING_DIFFUSE_BASE_PREFIX,
textureBaseUrl
);
}
}
if (entry.entryType === ENTRY_TYPE_MESH) {
const material = await this.#materialFactory.createMaterial(
materialDefinition,
textureUrls,
textures,
entry.usesVertexColors
);
const mesh = new Mesh(entry.geometry, material);
entry.parent.add(mesh);
meshes.push(mesh);
materials.push(material);
continue;
}
if (entry.entryType === ENTRY_TYPE_POINTS) {
const color = materialDefinition ? materialDefinition.diffuseColor : this.#defaultColor;
const material = new PointsMaterial(this.#webglContext, { color });
material.setOpacity(materialDefinition ? materialDefinition.opacity : DEFAULT_OPACITY);
const points = new Points(entry.geometry, material);
entry.parent.add(points);
meshes.push(points);
materials.push(material);
continue;
}
if (entry.entryType === ENTRY_TYPE_LINE) {
const color = materialDefinition ? materialDefinition.diffuseColor : this.#defaultColor;
const material = new SolidColorMaterial(this.#webglContext, { color });
material.setOpacity(materialDefinition ? materialDefinition.opacity : DEFAULT_OPACITY);
const line = new Line(entry.geometry, material);
entry.parent.add(line);
meshes.push(line);
materials.push(material);
}
}
return {
root,
meshes,
geometries,
materials,
textures
};
}
/**
* Resolves an asset path using an override map, when provided.
*
* @param {string} baseUrl - Base URL.
* @param {string} path - Asset path.
* @param {Map<string, string>} [assetUrlMap] - Asset URL map.
* @returns {string} - Asset URL resolved from `assetUrlMap`, when matched - otherwise resolved against `baseUrl`.
* @private
*/
static #resolveAssetUrl(baseUrl, path, assetUrlMap) {
if (assetUrlMap instanceof Map) {
const normalized = ObjMtlLoader.#normalizePath(path);
if (assetUrlMap.has(normalized)) {
return assetUrlMap.get(normalized);
}
const basename = ObjMtlLoader.#getBasename(normalized);
if (basename && assetUrlMap.has(basename)) {
return assetUrlMap.get(basename);
}
}
const resolved = ObjMtlLoader.#resolvePath(baseUrl, path);
if (assetUrlMap instanceof Map) {
const normalizedResolved = ObjMtlLoader.#normalizePath(resolved);
if (assetUrlMap.has(normalizedResolved)) {
return assetUrlMap.get(normalizedResolved);
}
const basenameResolved = ObjMtlLoader.#getBasename(normalizedResolved);
if (basenameResolved && assetUrlMap.has(basenameResolved)) {
return assetUrlMap.get(basenameResolved);
}
}
return resolved;
}
/**
* Resolves a base path from a URL string.
*
* @param {string} url - Input URL.
* @returns {string} - Base URL path or an empty string, when no slash is present.
* @private
*/
static #getBasePath(url) {
const lastSlashIndex = url.lastIndexOf(PATH_SEPARATOR);
if (lastSlashIndex === NOT_FOUND_INDEX) {
return DEFAULT_BASE_URL;
}
return url.slice(ZERO_VALUE, lastSlashIndex + BASE_PATH_SLICE_OFFSET);
}
/**
* Resolves a relative path against a base URL.
*
* @param {string} baseUrl - Base URL.
* @param {string} path - Path to resolve.
* @returns {string} - Resolved URL string, absolute paths are returned as-is - otherwise resolved against `baseUrl`.
* @private
*/
static #resolvePath(baseUrl, path) {
const normalizedBase = ObjMtlLoader.#normalizePath(baseUrl);
const normalizedPath = ObjMtlLoader.#normalizePath(path);
if (!normalizedPath) {
return normalizedBase;
}
if (ABSOLUTE_URL_REGEX.test(normalizedPath) || normalizedPath.startsWith(PATH_SEPARATOR)) {
return normalizedPath;
}
if (!normalizedBase) {
return normalizedPath;
}
const baseForCompare = ObjMtlLoader.#stripDotSlashPrefix(normalizedBase);
const pathForCompare = ObjMtlLoader.#stripDotSlashPrefix(normalizedPath);
if (baseForCompare && pathForCompare) {
const baseWithSeparator = baseForCompare.endsWith(PATH_SEPARATOR)
? baseForCompare
: baseForCompare + PATH_SEPARATOR;
if (pathForCompare === baseForCompare || pathForCompare.startsWith(baseWithSeparator)) {
return normalizedPath;
}
}
if (normalizedBase.endsWith(PATH_SEPARATOR) || normalizedPath.startsWith(PATH_SEPARATOR)) {
return normalizedBase + normalizedPath;
}
return normalizedBase + PATH_SEPARATOR + normalizedPath;
}
/**
* Removes a leading `./` prefix from a normalized path.
*
* @param {string} path - Normalized path to sanitize.
* @returns {string} - Path without a leading `./` prefix.
* @private
*/
static #stripDotSlashPrefix(path) {
if (path.startsWith(DOT_SLASH_PREFIX)) {
return path.slice(DOT_SLASH_PREFIX.length);
}
return path;
}
/**
* Normalizes a path string by: trimming, unquoting and deduplicating the relative slashes.
*
* @param {string} path - Input path.
* @returns {string} - Normalized path string.
* @private
*/
static #normalizePath(path) {
if (typeof path !== TYPEOF_STRING) {
return EMPTY_STRING;
}
let normalized = path.trim();
if (normalized.startsWith(QUOTE_TOKEN) && normalized.endsWith(QUOTE_TOKEN) && normalized.length > SECOND_INDEX) {
normalized = normalized.slice(SECOND_INDEX, normalized.length - SECOND_INDEX);
}
if (normalized.includes(BACKSLASH_SEPARATOR)) {
normalized = normalized.replace(BACKSLASH_REGEX, PATH_SEPARATOR);
}
if (!ABSOLUTE_URL_REGEX.test(normalized) && !normalized.startsWith(PATH_SEPARATOR)) {
normalized = normalized.replace(MULTIPLE_SLASHES_REGEX, PATH_SEPARATOR);
}
return normalized.trim();
}
/**
* Resolves texture URLs for all supported maps.
*
* @param {Object | null} definition - Parsed material definition.
* @param {string} textureBaseUrl - Base URL for textures.
* @param {Map<string, string>} [assetUrlMap] - Asset URL override map.
* @returns {Object | null} - Map of resolved URLs.
* @private
*/
static #resolveTextureUrls(definition, textureBaseUrl, assetUrlMap) {
if (!definition) {
return null;
}
return {
diffuse : definition.diffuseMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.diffuseMap.path, assetUrlMap)
: null,
ambient : definition.ambientMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.ambientMap.path, assetUrlMap)
: null,
specular : definition.specularMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.specularMap.path, assetUrlMap)
: null,
alpha : definition.alphaMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.alphaMap.path, assetUrlMap)
: null,
bump : definition.bumpMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.bumpMap.path, assetUrlMap)
: null,
displacement : definition.displacementMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.displacementMap.path, assetUrlMap)
: null,
reflection : definition.reflectionMap
? ObjMtlLoader.#resolveAssetUrl(textureBaseUrl, definition.reflectionMap.path, assetUrlMap)
: null
};
}
/**
* Returns a basename for the path.
*
* @param {string} path - Normalized path.
* @returns {string} - Basename, or empty string when not available.
* @private
*/
static #getBasename(path) {
if (!path) {
return EMPTY_STRING;
}
const index = path.lastIndexOf(PATH_SEPARATOR);
if (index === NOT_FOUND_INDEX) {
return path;
}
return path.slice(index + BASE_PATH_SLICE_OFFSET);
}
/**
* Returns a file entry from map using the normalized path or basename.
*
* @param {Map<string, File>} fileMap - File map.
* @param {string} path - File path.
* @returns {File | null} - Matched file entry by normalized path or basename or `null`, when not found.
* @private
*/
static #getFileFromMap(fileMap, path) {
if (!fileMap || !path) {
return null;
}
const normalized = ObjMtlLoader.#normalizePath(path);
if (fileMap.has(normalized)) {
return fileMap.get(normalized);
}
const basenameIndex = normalized.lastIndexOf(PATH_SEPARATOR);
const basename = basenameIndex === NOT_FOUND_INDEX
? normalized
: normalized.slice(basenameIndex + BASE_PATH_SLICE_OFFSET);
if (fileMap.has(basename)) {
return fileMap.get(basename);
}
return null;
}
}