UNPKG

terriajs

Version:

Geospatial data visualization platform.

264 lines 14.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { action, observable, runInAction, makeObservable } from "mobx"; import URI from "urijs"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import loadArrayBuffer from "../../../Core/loadArrayBuffer"; import loadBlob, { isZip, parseZipArrayBuffers } from "../../../Core/loadBlob"; import TerriaError, { TerriaErrorSeverity } from "../../../Core/TerriaError"; import { getName } from "../../../ModelMixins/CatalogMemberMixin"; import GltfMixin from "../../../ModelMixins/GltfMixin"; import AssImpCatalogItemTraits from "../../../Traits/TraitsClasses/AssImpCatalogItemTraits"; import CommonStrata from "../../Definition/CommonStrata"; import CreateModel from "../../Definition/CreateModel"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; // List of supported image formats from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types // + Cesium adds support for ktx2 const supportedTextureExtensions = [ "apng", "avif", "gif", "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "svg", "webp", "ktx2" ]; export default class AssImpCatalogItem extends GltfMixin(CreateModel(AssImpCatalogItemTraits)) { gltfModelUrl; static type = "assimp"; constructor(...args) { super(...args); makeObservable(this); } get type() { return AssImpCatalogItem.type; } hasLocalData = false; setFileInput(file) { const dataUrl = URL.createObjectURL(file); this.setTrait(CommonStrata.user, "urls", [dataUrl]); this.hasLocalData = true; } async forceLoadMapItems() { const urls = this.urls.length > 0 ? this.urls : filterOutUndefined([this.url]); if (urls.length === 0) return; let baseUrl = this.baseUrl; // If no baseUrl provided - but we have a single URL -> construct baseUrl from that if (!baseUrl && urls.length === 1) { const uri = new URI(urls[0]); baseUrl = uri.origin() + uri.directory() + "/"; } // TODO: revokeObjectURL() for all created objects /** Maps paths to absolute URLs * This is used to substitute paths in GlTf (eg buffer or image/texture paths) * - All local data (eg files output from assimp - or locally uploaded zip file) - use `createObjectURL` to get URL to local blob * - All remote data will use absolute URLs * * For example * - **Remote data** - explicitly defined in `this.url` or `this.urls` * - URL "http://localhost:3001/some-dir/some-file.jpg" (as defined in `url` or `urls`) * - path in GlTf = "some-file.jpg" * - So we replace gltf.images[i].uri "some-file.jpg" with "http://localhost:3001/some-dir/some-file.jpg" (using `dataUrls`) * - **Remote data** - implicitly references in 3D file * - URL "http://localhost:3001/some-dir/some-collada-file.dae" (as defined in `url` or `urls`) * - This Collada file internally references "some-file.jpg" * - `baseUrl` is equal to "http://localhost:3001/some-dir/" * - So we replace gltf.images[i].uri "some-file.jpg" with "http://localhost:3001/some-dir/some-file.jpg" (this is done post conversion to GlTf) * - **Local data** - from zip file * - Upload zip file with "some-file.jpg" * - Create local blob URL "blob:http://localhost:3001/some-blob-uuid" * - So we replace gltf.images[i].uri "some-file.jpg" with "blob:http://localhost:3001/some-blob-uuid" (using `dataUrls`) */ const dataUrls = new Map(); /** List of files to input into `assimpjs` * This will be populated with all files downloaded using `url` or `urls` - and all files from downloaded/uploaded zip files */ const fileArrayBuffers = []; let zipRootDir; await Promise.all(urls.map(async (url) => { // **Local data** - treat all URLs as zip if they have been uploaded if (isZip(url) || this.hasLocalData) { const blob = await loadBlob(proxyCatalogItemUrl(this, url)); const zipFiles = await parseZipArrayBuffers(blob); zipFiles.forEach((zipFile, i) => { if (i === 0 && zipFile.isDirectory) { // If the first entry is a directory, we treat it as a root // directory that contains all the other files. zipRootDir = zipFile.fileName; } if (zipFile.isDirectory) { // Directories have no data to process return; } fileArrayBuffers.push({ name: zipFile.fileName, arrayBuffer: zipFile.data }); // Because these unzipped files are local - we need to create URL to local blob const blob = new Blob([zipFile.data]); const dataUrl = URL.createObjectURL(blob); // Push filename -> local data blob URI let fileName = zipFile.fileName; if (zipRootDir) { // This zip appears to have contents inside a root // directory. Rewrite file names relative to this root so that // assimp can correctly resolve relatively referenced assets. fileName = zipFile.fileName.startsWith(zipRootDir) ? zipFile.fileName.slice(zipRootDir.length) : zipFile.fileName; } dataUrls.set(fileName, dataUrl); }); } // **Remote data** - explicitly defined in `url` or `urls` else { const arrayBuffer = await loadArrayBuffer(proxyCatalogItemUrl(this, url)); const uri = new URI(url); const name = uri.filename(); fileArrayBuffers.push({ name, arrayBuffer: new Uint8Array(arrayBuffer) }); // Because all these files are "remote", we want to substitute filename with absolute URL dataUrls.set(name, uri.absoluteTo(window.location.href).toString()); } })); // Init assimpjs const assimpjs = (await import("assimpjs")).default; const ajs = await assimpjs(); // Create assimpjs FileList object, and add the files const fileList = new ajs.FileList(); for (let i = 0; i < fileArrayBuffers.length; i++) { fileList.AddFile(fileArrayBuffers[i].name, new Uint8Array(fileArrayBuffers[i].arrayBuffer)); } // Convert files to GlTf 2 const result = ajs.ConvertFileList(fileList, "gltf2"); const fileCount = result.FileCount(); if (!result.IsSuccess() || fileCount === 0) { throw TerriaError.from(result.GetErrorCode(), { title: "Failed to convert files to GlTf" }); } /** This is used so we only set `this.gltfModelUrl` after process has finished */ let gltfModelUrl; /** List of unsupported texture URLs to show in warning message */ const unsupportedTextures = []; // Go through files backward - as GlTf file is first (i==0), followed by dependencies (eg buffers, textures) // As we may need to correct paths in GlTf file. Dependencies are stored in browser - so we need to use local blob object URL before processing GlTf file for (let i = fileCount - 1; i >= 0; i--) { const file = result.GetFile(i); const path = file.GetPath(); let arrayBuffer = file.GetContent(); // i === 0 is GlTf file // So we parse the file into JSON and edit paths for buffers, images, ... if (i === 0) { const file = new File([arrayBuffer], path); const gltfJson = JSON.parse(await file.text()); // Replace buffer file URIs // Buffer files are generated by Assimp - so we just replace the path with local Blob URL gltfJson.buffers?.forEach((buffer) => { if (!buffer.uri) return; const newUri = dataUrls.get(buffer.uri); console.log(`replacing GlTf buffer path \`${buffer.uri}\` with \`${newUri}\``); buffer.uri = newUri; }); // Replace image file URIs // Image files are external to Assimp - so we do a bit more wrangling: // - Replace back slashes with forward slash // - Remove leading "./" or "//" // - See dataUrls for info on URL transformation gltfJson.images?.forEach((image) => { if (!image.uri) return; // Replace back slashes with forward slash let newUrl = image.uri.replace(/\\/g, "/"); // Remove start "./" or "//" from uri if (newUrl.startsWith("//") || newUrl.startsWith("./")) { newUrl = newUrl.slice(2); } // If any unsupported image URIs are detected (by extension/suffix) then we show a warning message listing them if (!supportedTextureExtensions.some((ext) => ext === new URI(newUrl).suffix())) { unsupportedTextures.push(image.uri); } // Do we have substitute URL in dataUrls (see dataUrls for more info) // This covers: // - Remote data - explicitly defined in `url` or `urls` // - Local data - from zip file if (dataUrls.has(newUrl)) { newUrl = dataUrls.get(newUrl); } // No substitute URL - so resolve URL to baseUrl (if defined) // This covers: // - Remote data - implicitly defined in 3D file else if (baseUrl) { newUrl = new URI(newUrl).absoluteTo(baseUrl).toString(); } if (newUrl !== image.uri) { console.log(`replacing GlTf image path \`${image.uri}\` with \`${newUrl}\``); image.uri = newUrl; } }); // TODO: Replace KHR_techniques_webgl extension shader URIs? // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Archived/KHR_techniques_webgl/README.md#shader /** For some reason Cesium ignores textures if the KHR_materials_pbrSpecularGlossiness material extension is used and model has no normals. * * Here's the code that shades based on the diffuseTexture when that extension is in use: https://github.com/TerriaJS/cesium/blob/terriajs/Source/Scene/processPbrMaterials.js#L797 * And it's inside this conditional: https://github.com/TerriaJS/cesium/blob/terriajs/Source/Scene/processPbrMaterials.js#L771 * * For the moment - if we have images, we go through all materials and delete the extension */ if (gltfJson.images && gltfJson.images.length > 0) { gltfJson.materials?.forEach((material) => { if (material.extensions?.KHR_materials_pbrSpecularGlossiness) material.extensions.KHR_materials_pbrSpecularGlossiness = undefined; }); } // Turn GlTf back into array buffer - this overwrites existing GlTf arrayBuffer = new TextEncoder().encode(JSON.stringify(gltfJson)); } // Convert assimp output file to blob and create object URL const blob = new Blob([arrayBuffer]); const dataUrl = URL.createObjectURL(blob); // Add map from filePath to local blob URL // This will be used to substitute paths when processing GlTf file dataUrls.set(path, dataUrl); // GlTf file if (i === 0) { gltfModelUrl = dataUrl; } } runInAction(() => { this.gltfModelUrl = gltfModelUrl; }); if (unsupportedTextures.length > 0) throw new TerriaError({ severity: TerriaErrorSeverity.Warning, title: "Unsupported texture formats", message: `Catalog item \`"${getName(this)}"\` has unsupported texture formats for the following: \n${unsupportedTextures.map((path) => `- \`${path}\`\n`)}\n\nThe model may not display correctly. \n\nList of supported formats: ${supportedTextureExtensions .map((ext) => `\`${ext}\``) .join(", ")}` }); } } __decorate([ observable ], AssImpCatalogItem.prototype, "gltfModelUrl", void 0); __decorate([ observable ], AssImpCatalogItem.prototype, "hasLocalData", void 0); __decorate([ action ], AssImpCatalogItem.prototype, "setFileInput", null); //# sourceMappingURL=AssImpCatalogItem.js.map