diff --git a/.jshintrc b/.jshintrc index ec9cff1..a8e67b5 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,7 @@ "globals" : { "OV" : true, "THREE" : false, + "TextEncoder" : false, "TextDecoder" : false, "XMLHttpRequest" : false, "FileReader" : false, diff --git a/README.md b/README.md index 9d6ad97..9531bf3 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,29 @@ This repository contains the source code of the https://3dviewer.net website. Th [![Build status](https://ci.appveyor.com/api/projects/status/exypq43a8kjby5n0?svg=true)](https://ci.appveyor.com/project/kovacsv/online3dviewer) [![Build Status](https://travis-ci.com/kovacsv/Online3DViewer.svg?branch=master)](https://travis-ci.com/kovacsv/Online3DViewer) +## Documentation + +- [User Documentation](https://3dviewer.net/info) +- [Developer Documentation](https://github.com/kovacsv/Online3DViewer/wiki) + ## Supported file formats ### Import - obj (with mtl and texture) - 3ds (with texture) -- stl (ascii and binary) -- ply (ascii and binary) -- gltf (ascii and binary) -- off (ascii only) +- stl (text and binary) +- ply (text and binary) +- gltf (text and binary) +- off (text only) ### Export - obj (with mtl) -- stl (ascii and binary) -- ply (ascii and binary) -- off (ascii only) +- stl (text and binary) +- ply (text and binary) +- gltf (text and binary) +- off (text only) ## Features diff --git a/appveyor.yml b/appveyor.yml index d7c9c6d..4717929 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,7 +16,7 @@ test_script: - npm run test artifacts: - - path: build\package\* + - path: build\final\package\* deploy: description: 'News:\n- ...' diff --git a/package.json b/package.json index b0487e4..2023097 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "online-3d-viewer", "description": "Online 3D Viewer", - "version": "0.7.2", + "version": "0.7.3", "repository": "github:kovacsv/Online3DViewer", "license": "MIT", "devDependencies": { diff --git a/source/export/exporter.js b/source/export/exporter.js index 0c1c3b9..ddc3c44 100644 --- a/source/export/exporter.js +++ b/source/export/exporter.js @@ -11,13 +11,13 @@ OV.Exporter = class ]; } - Export (model, format, extension, callbacks) + Export (model, format, extension) { let files = []; for (let i = 0; i < this.exporters.length; i++) { let exporter = this.exporters[i]; if (exporter.CanExport (format, extension)) { - exporter.Export (model, format, files, callbacks); + exporter.Export (model, format, files); break; } } diff --git a/source/export/exporterbase.js b/source/export/exporterbase.js index c906e78..b35c7ab 100644 --- a/source/export/exporterbase.js +++ b/source/export/exporterbase.js @@ -42,7 +42,7 @@ OV.ExporterBase = class { constructor () { - this.callbacks = null; + } CanExport (format, extension) @@ -50,9 +50,8 @@ OV.ExporterBase = class return false; } - Export (model, format, files, callbacks) + Export (model, format, files) { - this.callbacks = callbacks; this.ExportContent (model, format, files); } diff --git a/source/export/exportergltf.js b/source/export/exportergltf.js index 39b8498..709a489 100644 --- a/source/export/exportergltf.js +++ b/source/export/exportergltf.js @@ -72,18 +72,31 @@ OV.ExporterGltf = class extends OV.ExporterBase ExportBinaryContent (model, files) { + function AlignToBoundary (size) + { + let remainder = size % 4; + if (remainder === 0) { + return size; + } + return size + (4 - remainder); + } + + function WriteCharacters (writer, char, count) + { + for (let i = 0; i < count; i++) { + writer.WriteUnsignedCharacter8 (char); + } + } + let glbFile = new OV.ExportedFile ('model.glb'); files.push (glbFile); let meshDataArr = this.GetMeshData (model); let mainBuffer = this.GetMainBuffer (meshDataArr); let mainJson = this.GetMainJson (meshDataArr); - mainJson.buffers.push ({ - byteLength : mainBuffer.byteLength - }); let textureBuffers = []; - let obj = this; + let textureOffset = mainBuffer.byteLength; let fileNameToIndex = []; this.ExportMaterials (model, mainJson, function (texture) { @@ -91,20 +104,17 @@ OV.ExporterGltf = class extends OV.ExporterBase let extension = OV.GetFileExtension (texture.name); let textureIndex = fileNameToIndex[fileName]; if (textureIndex === undefined) { - let bufferIndex = mainJson.buffers.length; let bufferViewIndex = mainJson.bufferViews.length; textureIndex = mainJson.textures.length; fileNameToIndex[fileName] = textureIndex; - let textureBuffer = obj.callbacks.getTextureBuffer (texture.name); + let textureBuffer = texture.buffer; textureBuffers.push (textureBuffer); - mainJson.buffers.push ({ - byteLength : textureBuffer.byteLength - }); mainJson.bufferViews.push ({ - buffer : bufferIndex, - byteOffset : 0, + buffer : 0, + byteOffset : textureOffset, byteLength : textureBuffer.byteLength }); + textureOffset += textureBuffer.byteLength; mainJson.images.push ({ bufferView : bufferViewIndex, mimeType : 'image/' + extension @@ -116,34 +126,42 @@ OV.ExporterGltf = class extends OV.ExporterBase return textureIndex; }); - let mainJsonString = JSON.stringify (mainJson, null, 4); - let glbSize = 12 + 8 + mainJsonString.length; - for (let i = 0; i < mainJson.buffers.length; i++) { - glbSize += 8 + mainJson.buffers[i].byteLength; + let mainBinaryBufferLength = mainBuffer.byteLength; + for (let i = 0; i < textureBuffers.length; i++) { + let textureBuffer = textureBuffers[i]; + mainBinaryBufferLength += textureBuffer.byteLength; } + let mainBinaryBufferAlignedLength = AlignToBoundary (mainBinaryBufferLength); + mainJson.buffers.push ({ + byteLength : mainBinaryBufferAlignedLength + }); + let mainJsonString = JSON.stringify (mainJson); + let mainJsonBuffer = OV.Utf8StringToArrayBuffer (mainJsonString); + let mainJsonBufferLength = mainJsonBuffer.byteLength; + let mainJsonBufferAlignedLength = AlignToBoundary (mainJsonBufferLength); + + let glbSize = 12 + 8 + mainJsonBufferAlignedLength + 8 + mainBinaryBufferAlignedLength; let glbWriter = new OV.BinaryWriter (glbSize, true); glbWriter.WriteUnsignedInteger32 (0x46546C67); glbWriter.WriteUnsignedInteger32 (2); glbWriter.WriteUnsignedInteger32 (glbSize); - glbWriter.WriteUnsignedInteger32 (mainJsonString.length); + glbWriter.WriteUnsignedInteger32 (mainJsonBufferAlignedLength); glbWriter.WriteUnsignedInteger32 (0x4E4F534A); - for (let i = 0; i < mainJsonString.length; i++) { - glbWriter.WriteUnsignedCharacter8 (mainJsonString.charCodeAt (i)); - } + glbWriter.WriteArrayBuffer (mainJsonBuffer); + WriteCharacters (glbWriter, 32, mainJsonBufferAlignedLength - mainJsonBufferLength); - glbWriter.WriteUnsignedInteger32 (mainBuffer.byteLength); + glbWriter.WriteUnsignedInteger32 (mainBinaryBufferAlignedLength); glbWriter.WriteUnsignedInteger32 (0x004E4942); glbWriter.WriteArrayBuffer (mainBuffer); for (let i = 0; i < textureBuffers.length; i++) { let textureBuffer = textureBuffers[i]; - glbWriter.WriteUnsignedInteger32 (textureBuffer.byteLength); - glbWriter.WriteUnsignedInteger32 (0x004E4942); glbWriter.WriteArrayBuffer (textureBuffer); } + WriteCharacters (glbWriter, 0, mainBinaryBufferAlignedLength - mainBinaryBufferLength); glbFile.SetContent (glbWriter.GetBuffer ()); } @@ -207,6 +225,16 @@ OV.ExporterGltf = class extends OV.ExporterBase GetMainJson (meshDataArr) { + function AddBufferView (mainJson, offset, length) + { + mainJson.bufferViews.push ({ + buffer : 0, + byteOffset : offset, + byteLength : length, + }); + return offset + length; + } + let mainJson = { asset : { version : '2.0' @@ -239,10 +267,14 @@ OV.ExporterGltf = class extends OV.ExporterBase let primitives = meshData.buffer.primitives; for (let primitiveIndex = 0; primitiveIndex < primitives.length; primitiveIndex++) { let primitive = primitives[primitiveIndex]; - let bufferViewIndex = mainJson.bufferViews.length; - let accessorIndex = mainJson.accessors.length; - let accessorOffset = 0; + let bufferViewIndex = mainJson.bufferViews.length; + let bufferViewOffset = meshData.offsets[primitiveIndex]; + bufferViewOffset = AddBufferView (mainJson, bufferViewOffset, primitive.indices.length * this.components.index.size); + bufferViewOffset = AddBufferView (mainJson, bufferViewOffset, primitive.vertices.length * this.components.number.size); + bufferViewOffset = AddBufferView (mainJson, bufferViewOffset, primitive.normals.length * this.components.number.size); + + let accessorIndex = mainJson.accessors.length; let jsonPrimitive = { attributes : { POSITION : accessorIndex + 1, @@ -253,48 +285,39 @@ OV.ExporterGltf = class extends OV.ExporterBase material : primitive.material }; - mainJson.bufferViews.push ({ - buffer : 0, - byteOffset : meshData.offsets[primitiveIndex], - byteLength : meshData.sizes[primitiveIndex] - }); - + let bounds = primitive.GetBounds (); mainJson.accessors.push ({ bufferView : bufferViewIndex, - byteOffset : accessorOffset, + byteOffset : 0, componentType : this.components.index.type, count : primitive.indices.length, type : 'SCALAR' }); - accessorOffset += primitive.indices.length * this.components.index.size; - mainJson.accessors.push ({ - bufferView : bufferViewIndex, - byteOffset : accessorOffset, + bufferView : bufferViewIndex + 1, + byteOffset : 0, componentType : this.components.number.type, count : primitive.vertices.length / 3, + min : bounds.min, + max : bounds.max, type : 'VEC3' }); - accessorOffset += primitive.vertices.length * this.components.number.size; - mainJson.accessors.push ({ - bufferView : bufferViewIndex, - byteOffset : accessorOffset, + bufferView : bufferViewIndex + 2, + byteOffset : 0, componentType : this.components.number.type, count : primitive.normals.length / 3, type : 'VEC3' }); - accessorOffset += primitive.normals.length * this.components.number.size; - if (primitive.uvs.length > 0) { + bufferViewOffset = AddBufferView (mainJson, bufferViewOffset, primitive.uvs.length * this.components.number.size); mainJson.accessors.push ({ - bufferView : bufferViewIndex, - byteOffset : accessorOffset, + bufferView : bufferViewIndex + 3, + byteOffset : 0, componentType : this.components.number.type, count : primitive.uvs.length / 2, type : 'VEC2' }); - accessorOffset += primitive.uvs.length * this.components.number.size; jsonPrimitive.attributes.TEXCOORD_0 = accessorIndex + 3; } @@ -331,7 +354,7 @@ OV.ExporterGltf = class extends OV.ExporterBase function GetTextureParams (mainJson, texture, addTexture) { - if (texture === null || texture.name === null || texture.url === null) { + if (texture === null || !texture.IsValid ()) { return null; } @@ -343,11 +366,18 @@ OV.ExporterGltf = class extends OV.ExporterBase } let textureIndex = addTexture (texture); - let textureParams = { + let textureParams = { index : textureIndex }; if (texture.HasTransformation ()) { + let extensionName = 'KHR_texture_transform'; + if (mainJson.extensionsUsed === undefined) { + mainJson.extensionsUsed = []; + } + if (mainJson.extensionsUsed.indexOf (extensionName) === -1) { + mainJson.extensionsUsed.push (extensionName); + } textureParams.extensions = { KHR_texture_transform : { offset : [texture.offset.x, -texture.offset.y], @@ -368,12 +398,11 @@ OV.ExporterGltf = class extends OV.ExporterBase roughnessFactor : 1.0 }, emissiveFactor : ColorToRGB (material.emissive), - alphaMode : 'OPAQUE', - alphaCutoff : material.alphaTest + alphaMode : 'OPAQUE' }; if (material.transparent) { - // TODO: mask? + // TODO: mask, alphaCutoff? jsonMaterial.alphaMode = 'BLEND'; } diff --git a/source/export/exporterobj.js b/source/export/exporterobj.js index 0267ea7..98b65f0 100644 --- a/source/export/exporterobj.js +++ b/source/export/exporterobj.js @@ -14,18 +14,19 @@ OV.ExporterObj = class extends OV.ExporterBase { function WriteTexture (mtlWriter, keyword, texture, files) { - if (texture !== null && texture.name !== null && texture.url !== null) { - let fileName = OV.GetFileName (texture.name); - mtlWriter.WriteArrayLine ([keyword, fileName]); + if (texture === null || !texture.IsValid ()) { + return; + } + let fileName = OV.GetFileName (texture.name); + mtlWriter.WriteArrayLine ([keyword, fileName]); - let fileIndex = files.findIndex (function (file) { - return file.GetName () === fileName; - }); - if (fileIndex === -1) { - let textureFile = new OV.ExportedFile (fileName); - textureFile.SetUrl (texture.url); - files.push (textureFile); - } + let fileIndex = files.findIndex (function (file) { + return file.GetName () === fileName; + }); + if (fileIndex === -1) { + let textureFile = new OV.ExportedFile (fileName); + textureFile.SetUrl (texture.url); + files.push (textureFile); } } diff --git a/source/external/three.converter.js b/source/external/three.converter.js index f8b16d7..2020e6f 100644 --- a/source/external/three.converter.js +++ b/source/external/three.converter.js @@ -15,7 +15,7 @@ OV.ConvertModelToThreeMeshes = function (model, callbacks) function LoadTexture (threeMaterial, texture, onLoad) { - if (texture === null || texture.url === null) { + if (texture === null || !texture.IsValid ()) { return; } let loader = new THREE.TextureLoader (); diff --git a/source/external/three.model.loader.js b/source/external/three.model.loader.js index c8ca354..1a80d44 100644 --- a/source/external/three.model.loader.js +++ b/source/external/three.model.loader.js @@ -53,8 +53,8 @@ OV.ThreeModelLoader = class obj.OnModelImported (importResult); ready (); }, - error : function (importerError) { - obj.callbacks.onLoadError (importerError); + error : function (importError) { + obj.callbacks.onLoadError (importError); obj.inProgress = false; ready (); } diff --git a/source/import/importer.js b/source/import/importer.js index d8ab5a5..0f65848 100644 --- a/source/import/importer.js +++ b/source/import/importer.js @@ -179,14 +179,29 @@ OV.FileList = class } }; -OV.ImporterErrorCode = +OV.RevokeModelUrls = function (model) +{ + if (model === null) { + return; + } + for (let i = 0; i < model.MaterialCount (); i++) { + let material = model.GetMaterial (i); + material.EnumerateTextureMaps (function (texture) { + if (texture.url !== null) { + OV.RevokeObjectUrl (texture.url); + } + }); + } +}; + +OV.ImportErrorCode = { NoImportableFile : 1, ImportFailed : 2, UnknownError : 3 }; -OV.ImporterError = class +OV.ImportError = class { constructor (code, message) { @@ -236,8 +251,8 @@ OV.Importer = class Import (callbacks) { let mainFile = this.fileList.GetMainFile (); - if (mainFile === null || mainFile.file.content === null) { - callbacks.error (new OV.ImporterError (OV.ImporterErrorCode.NoImportableFile, null)); + if (mainFile === null || mainFile.file === null || mainFile.file.content === null) { + callbacks.error (new OV.ImportError (OV.ImportErrorCode.NoImportableFile, null)); return; } @@ -245,9 +260,6 @@ OV.Importer = class result.mainFile = mainFile.file.name; result.usedFiles.push (mainFile.file.name); - let fileNameToContent = {}; - let textureNameToUrl = {}; - let obj = this; let importer = mainFile.importer; importer.Import (mainFile.file.content, mainFile.file.extension, { @@ -256,47 +268,24 @@ OV.Importer = class material.diffuse = new OV.Color (200, 200, 200); return material; }, - getFileContent : function (filePath) { - let fileName = OV.GetFileName (filePath); - let fileContent = fileNameToContent[fileName]; - if (fileContent === undefined) { - let file = obj.fileList.FindFileByPath (filePath); - if (file === null || file.content === null) { - result.missingFiles.push (fileName); - obj.missingFiles.push (fileName); - fileContent = null; - } else { - result.usedFiles.push (fileName); - fileContent = file.content; - } - fileNameToContent[fileName] = fileContent; + getFileBuffer : function (fileName) { + let fileBuffer = null; + let file = obj.fileList.FindFileByPath (fileName); + if (file === null || file.content === null) { + result.missingFiles.push (fileName); + obj.missingFiles.push (fileName); + fileBuffer = null; + } else { + result.usedFiles.push (fileName); + fileBuffer = file.content; } - return fileContent; - }, - getTextureObjectUrl : function (filePath) { - let fileName = OV.GetFileName (filePath); - let textureUrl = textureNameToUrl[fileName]; - if (textureUrl === undefined) { - let file = obj.fileList.FindFileByPath (filePath); - if (file === null || file.content === null) { - result.missingFiles.push (fileName); - obj.missingFiles.push (fileName); - textureUrl = null; - } else { - result.usedFiles.push (fileName); - let blob = new Blob ([file.content]); - let blobURL = URL.createObjectURL (blob); - textureUrl = blobURL; - } - textureNameToUrl[fileName] = textureUrl; - } - return textureUrl; + return fileBuffer; } }); if (importer.IsError ()) { let message = importer.GetMessage (); - callbacks.error (new OV.ImporterError (OV.ImporterErrorCode.ImportFailed, message)); + callbacks.error (new OV.ImportError (OV.ImportErrorCode.ImportFailed, message)); return; } @@ -350,5 +339,5 @@ OV.Importer = class IsOnlyFileSource (source) { return this.fileList.IsOnlySource (source); - } + } }; diff --git a/source/import/importer3ds.js b/source/import/importer3ds.js index cdde82b..5973d3d 100644 --- a/source/import/importer3ds.js +++ b/source/import/importer3ds.js @@ -177,9 +177,12 @@ OV.Importer3ds = class extends OV.ImporterBase this.ReadChunks (reader, endByte, function (chunkId, chunkLength) { if (chunkId === OV.CHUNK3DS.MAT_TEXMAP_NAME) { let textureName = obj.ReadName (reader); - let textureObjectUrl = obj.callbacks.getTextureObjectUrl (textureName); + let textureBuffer = obj.GetTextureBuffer (textureName); texture.name = textureName; - texture.url = textureObjectUrl; + if (textureBuffer !== null) { + texture.url = textureBuffer.url; + texture.buffer = textureBuffer.buffer; + } } else if (chunkId === OV.CHUNK3DS.MAT_TEXMAP_UOFFSET) { texture.offset.x = reader.ReadFloat32 (); } else if (chunkId === OV.CHUNK3DS.MAT_TEXMAP_VOFFSET) { @@ -336,7 +339,7 @@ OV.Importer3ds = class extends OV.ImporterBase let v0 = reader.ReadUnsignedInteger16 (); let v1 = reader.ReadUnsignedInteger16 (); let v2 = reader.ReadUnsignedInteger16 (); - let flags = reader.ReadUnsignedInteger16 (); + reader.ReadUnsignedInteger16 (); // flags mesh.AddTriangle (new OV.Triangle (v0, v1, v2)); } diff --git a/source/import/importerbase.js b/source/import/importerbase.js index ae9e584..42fc726 100644 --- a/source/import/importerbase.js +++ b/source/import/importerbase.js @@ -1,18 +1,61 @@ +OV.ImporterBuffers = class +{ + constructor (getBufferCallback) + { + this.getBufferCallback = getBufferCallback; + this.fileBuffers = {}; + this.textureBuffers = {}; + } + + GetFileBuffer (filePath) + { + let fileName = OV.GetFileName (filePath); + let buffer = this.fileBuffers[fileName]; + if (buffer === undefined) { + buffer = this.getBufferCallback (fileName); + this.fileBuffers[fileName] = buffer; + } + return buffer; + } + + GetTextureBuffer (filePath) + { + let fileName = OV.GetFileName (filePath); + let buffer = this.textureBuffers[fileName]; + if (buffer === undefined) { + let texBuffer = this.getBufferCallback (fileName); + if (texBuffer !== null) { + buffer = { + url : OV.CreateObjectUrl (texBuffer), + buffer : texBuffer + }; + } else { + buffer = null; + } + this.textureBuffers[fileName] = buffer; + } + return buffer; + } +}; + OV.ImporterBase = class { constructor () { this.extension = null; this.callbacks = null; + this.buffers = null; this.model = null; this.error = null; this.message = null; + this.buffers = null; } Import (content, extension, callbacks) { this.extension = extension; this.callbacks = callbacks; + this.buffers = new OV.ImporterBuffers (this.callbacks.getFileBuffer); this.model = new OV.Model (); this.error = false; this.message = null; @@ -31,6 +74,16 @@ OV.ImporterBase = class OV.FinalizeModel (this.model, this.callbacks.getDefaultMaterial); } + GetFileBuffer (filePath) + { + return this.buffers.GetFileBuffer (filePath); + } + + GetTextureBuffer (filePath) + { + return this.buffers.GetTextureBuffer (filePath); + } + ResetState () { diff --git a/source/import/importergltf.js b/source/import/importergltf.js index f8d5017..6095dee 100644 --- a/source/import/importergltf.js +++ b/source/import/importergltf.js @@ -351,12 +351,14 @@ OV.ImporterGltf = class extends OV.ImporterBase this.bufferContents = null; this.gltfExtensions = null; + this.imageIndexToTextureParams = null; } ResetState () { this.bufferContents = []; this.gltfExtensions = new OV.GltfExtensions (); + this.imageIndexToTextureParams = {}; } CanImportExtension (extension) @@ -400,10 +402,13 @@ OV.ImporterGltf = class extends OV.ImporterBase let buffer = null; let gltfBuffer = gltf.buffers[i]; let base64Buffer = OV.Base64DataURIToArrayBuffer (gltfBuffer.uri); - if (base64Buffer === null) { - buffer = this.callbacks.getFileContent (gltfBuffer.uri); - } else { + if (base64Buffer !== null) { buffer = base64Buffer.buffer; + } else { + let fileBuffer = this.GetFileBuffer (gltfBuffer.uri); + if (fileBuffer !== null) { + buffer = fileBuffer; + } } if (buffer === null) { this.SetError (); @@ -613,31 +618,48 @@ OV.ImporterGltf = class extends OV.ImporterBase let texture = new OV.TextureMap (); let gltfTexture = gltf.textures[gltfTextureRef.index]; - let gltfImage = gltf.images[gltfTexture.source]; + let gltfImageIndex = gltfTexture.source; + let gltfImage = gltf.images[gltfImageIndex]; - let textureIndexString = gltfTexture.source.toString (); - if (gltfImage.uri !== undefined) { - let base64Buffer = OV.Base64DataURIToArrayBuffer (gltfImage.uri); - if (base64Buffer !== null) { - let blob = new Blob ([base64Buffer.buffer], { type : base64Buffer.mimeType }); - texture.name = 'Embedded_' + textureIndexString + GetTextureFileExtension (base64Buffer.mimeType); - texture.url = URL.createObjectURL (blob); - } else { - let textureObjectUrl = this.callbacks.getTextureObjectUrl (gltfImage.uri); - texture.name = gltfImage.uri; - texture.url = textureObjectUrl; - } - } else if (gltfImage.bufferView !== undefined) { - let bufferView = gltf.bufferViews[gltfImage.bufferView]; - let reader = this.GetReaderFromBufferView (bufferView); - if (reader !== null) { - let buffer = reader.ReadArrayBuffer (bufferView.byteLength); - let blob = new Blob ([buffer], { type: gltfImage.mimeType }); - texture.name = 'Binary_' + textureIndexString + GetTextureFileExtension (gltfImage.mimeType); - texture.url = URL.createObjectURL (blob); + let textureParams = this.imageIndexToTextureParams[gltfImageIndex]; + if (textureParams === undefined) { + textureParams = { + name : null, + url : null, + buffer : null + }; + let textureIndexString = gltfImageIndex.toString (); + if (gltfImage.uri !== undefined) { + let base64Buffer = OV.Base64DataURIToArrayBuffer (gltfImage.uri); + if (base64Buffer !== null) { + textureParams.name = 'Embedded_' + textureIndexString + GetTextureFileExtension (base64Buffer.mimeType); + textureParams.url = OV.CreateObjectUrlWithMimeType (base64Buffer.buffer, base64Buffer.mimeType); + textureParams.buffer = base64Buffer.buffer; + } else { + let textureBuffer = this.GetTextureBuffer (gltfImage.uri); + textureParams.name = gltfImage.uri; + if (textureBuffer !== null) { + textureParams.url = textureBuffer.url; + textureParams.buffer = textureBuffer.buffer; + } + } + } else if (gltfImage.bufferView !== undefined) { + let bufferView = gltf.bufferViews[gltfImage.bufferView]; + let reader = this.GetReaderFromBufferView (bufferView); + if (reader !== null) { + let buffer = reader.ReadArrayBuffer (bufferView.byteLength); + textureParams.name = 'Binary_' + textureIndexString + GetTextureFileExtension (gltfImage.mimeType); + textureParams.url = OV.CreateObjectUrlWithMimeType (buffer, gltfImage.mimeType); + textureParams.buffer = buffer; + } } + this.imageIndexToTextureParams[gltfImageIndex] = textureParams; } + texture.name = textureParams.name; + texture.url = textureParams.url; + texture.buffer = textureParams.buffer; + this.gltfExtensions.ProcessTexture (gltfTextureRef, texture); return texture; } @@ -741,9 +763,6 @@ OV.ImporterGltf = class extends OV.ImporterBase let hasNormals = (primitive.attributes.NORMAL !== undefined); let hasUVs = (primitive.attributes.TEXCOORD_0 !== undefined); let hasIndices = (primitive.indices !== undefined); - if (!hasVertices) { - return; - } let mode = OV.GltfRenderMode.TRIANGLES; if (primitive.mode !== undefined) { @@ -766,6 +785,8 @@ OV.ImporterGltf = class extends OV.ImporterBase reader.EnumerateData (function (data) { mesh.AddVertex (data); }); + } else { + return; } if (hasNormals) { diff --git a/source/import/importerobj.js b/source/import/importerobj.js index 40b9aff..e081c20 100644 --- a/source/import/importerobj.js +++ b/source/import/importerobj.js @@ -166,13 +166,15 @@ OV.ImporterObj = class extends OV.ImporterBase ); } - function CreateTexture (keyword, line, callbacks) + function CreateTexture (importer, keyword, line) { let texture = new OV.TextureMap (); - texture.name = OV.NameFromLine (line, keyword.length, '#'); - let textureObjectUrl = callbacks.getTextureObjectUrl (texture.name); - if (textureObjectUrl !== null) { - texture.url = textureObjectUrl; + let textureName = OV.NameFromLine (line, keyword.length, '#'); + let textureBuffer = importer.GetTextureBuffer (textureName); + texture.name = textureName; + if (textureBuffer !== null) { + texture.url = textureBuffer.url; + texture.buffer = textureBuffer.buffer; } return texture; } @@ -206,9 +208,9 @@ OV.ImporterObj = class extends OV.ImporterBase return true; } let fileName = OV.NameFromLine (line, keyword.length, '#'); - let fileContent = this.callbacks.getFileContent (fileName); - if (fileContent !== null) { - OV.ReadLines (fileContent, function (line) { + let fileBuffer = this.GetFileBuffer (fileName); + if (fileBuffer !== null) { + OV.ReadLines (fileBuffer, function (line) { if (!obj.IsError ()) { obj.ProcessLine (line); } @@ -219,19 +221,19 @@ OV.ImporterObj = class extends OV.ImporterBase if (this.currentMaterial === null || parameters.length === 0) { return true; } - this.currentMaterial.diffuseMap = CreateTexture (keyword, line, this.callbacks); + this.currentMaterial.diffuseMap = CreateTexture (this, keyword, line); return true; } else if (keyword === 'map_ks') { if (this.currentMaterial === null || parameters.length === 0) { return true; } - this.currentMaterial.specularMap = CreateTexture (keyword, line, this.callbacks); + this.currentMaterial.specularMap = CreateTexture (this, keyword, line); return true; } else if (keyword === 'map_bump' || keyword === 'bump') { if (this.currentMaterial === null || parameters.length === 0) { return true; } - this.currentMaterial.bumpMap = CreateTexture (keyword, line, this.callbacks); + this.currentMaterial.bumpMap = CreateTexture (this, keyword, line); return true; } else if (keyword === 'ka') { if (this.currentMaterial === null || parameters.length < 3) { diff --git a/source/io/bufferutils.js b/source/io/bufferutils.js index 17965e5..d8690c9 100644 --- a/source/io/bufferutils.js +++ b/source/io/bufferutils.js @@ -24,6 +24,12 @@ OV.AsciiStringToArrayBuffer = function (str) return buffer; }; +OV.Utf8StringToArrayBuffer = function (str) +{ + let encoder = new TextEncoder (); + return encoder.encode (str); +}; + OV.Base64DataURIToArrayBuffer = function (uri) { let dataPrefix = 'data:'; @@ -54,3 +60,22 @@ OV.Base64DataURIToArrayBuffer = function (uri) buffer : buffer }; }; + +OV.CreateObjectUrl = function (content) +{ + let blob = new Blob ([content]); + let url = URL.createObjectURL (blob); + return url; +}; + +OV.CreateObjectUrlWithMimeType = function (content, mimeType) +{ + let blob = new Blob ([content], { type : mimeType }); + let url = URL.createObjectURL (blob); + return url; +}; + +OV.RevokeObjectUrl = function (url) +{ + URL.revokeObjectURL (url); +}; diff --git a/source/model/meshbuffer.js b/source/model/meshbuffer.js index df203a4..d4f019f 100644 --- a/source/model/meshbuffer.js +++ b/source/model/meshbuffer.js @@ -9,6 +9,22 @@ OV.MeshPrimitiveBuffer = class this.material = null; } + GetBounds () + { + let min = [Infinity, Infinity, Infinity]; + let max = [-Infinity, -Infinity, -Infinity]; + for (let i = 0; i < this.vertices.length / 3; i++) { + for (let j = 0; j < 3; j++) { + min[j] = Math.min (min[j], this.vertices[i * 3 + j]); + max[j] = Math.max (max[j], this.vertices[i * 3 + j]); + } + } + return { + min : min, + max : max + }; + } + GetByteLength (indexTypeSize, numberTypeSize) { let indexCount = this.indices.length; diff --git a/source/model/modelentities.js b/source/model/modelentities.js index 39bbe09..18c2076 100644 --- a/source/model/modelentities.js +++ b/source/model/modelentities.js @@ -54,11 +54,17 @@ OV.TextureMap = class { this.name = null; this.url = null; + this.buffer = null; this.offset = new OV.Coord2D (0.0, 0.0); this.scale = new OV.Coord2D (1.0, 1.0); this.rotation = 0.0; // radians } + IsValid () + { + return this.name !== null && this.url !== null && this.buffer !== null; + } + HasTransformation () { if (!OV.CoordIsEqual2D (this.offset, new OV.Coord2D (0.0, 0.0))) { @@ -78,6 +84,7 @@ OV.TextureMap = class let cloned = new OV.TextureMap (); cloned.name = this.name; cloned.url = this.url; + cloned.buffer = this.buffer; cloned.offset = this.offset.Clone (); cloned.scale = this.scale.Clone (); cloned.rotation = this.rotation; @@ -110,6 +117,25 @@ OV.Material = class this.multiplyDiffuseMap = false; } + EnumerateTextureMaps (enumerator) + { + if (this.diffuseMap !== null) { + enumerator (this.diffuseMap); + } + if (this.specularMap !== null) { + enumerator (this.specularMap); + } + if (this.bumpMap !== null) { + enumerator (this.bumpMap); + } + if (this.normalMap !== null) { + enumerator (this.normalMap); + } + if (this.emissiveMap !== null) { + enumerator (this.emissiveMap); + } + } + Clone () { let cloned = new OV.Material (); diff --git a/source/viewer/domviewer.js b/source/viewer/domviewer.js index b606cb4..fa1a557 100644 --- a/source/viewer/domviewer.js +++ b/source/viewer/domviewer.js @@ -42,7 +42,7 @@ OV.Init3DViewerElements = function () onTextureLoaded : function () { viewer.Render (); }, - onLoadError : function (importerError) { + onLoadError : function (importError) { progressDiv.innerHTML = 'Unknown error.'; }, }); diff --git a/test/package.json b/test/package.json deleted file mode 100644 index a91072a..0000000 --- a/test/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "mocha": "*" - }, - "scripts": { - "test": "mocha" - } -} diff --git a/test/test.js b/test/test.js index 4f1541c..5ef2ef2 100644 --- a/test/test.js +++ b/test/test.js @@ -1,20 +1,7 @@ var fs = require ('fs'); process.chdir (__dirname); -global.atob = function (base64String) { - return Buffer.from (base64String, 'base64').toString('binary') -}; - -global.Blob = function () { - -}; - -global.URL = { - createObjectURL : function () { - return 'ObjectUrl'; - } -} - +require ('./utils/globals.js'); require ('./utils/importall.js'); var testDirName = './tests/'; var files = fs.readdirSync (testDirName, { withFileTypes: true }); diff --git a/test/testfiles/3ds/wrong.3ds b/test/testfiles/3ds/wrong.3ds new file mode 100644 index 0000000..a5ee7ab --- /dev/null +++ b/test/testfiles/3ds/wrong.3ds @@ -0,0 +1 @@ +invalid 3ds file diff --git a/test/testfiles/obj/rectangle_with_texture.mtl b/test/testfiles/obj/rectangle_with_texture.mtl new file mode 100644 index 0000000..a6f4d35 --- /dev/null +++ b/test/testfiles/obj/rectangle_with_texture.mtl @@ -0,0 +1,5 @@ +newmtl Material +Ka 0.000000 0.000000 0.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.000000 0.000000 0.000000 +map_Kd texture.png diff --git a/test/testfiles/obj/rectangle_with_texture.obj b/test/testfiles/obj/rectangle_with_texture.obj new file mode 100644 index 0000000..9cf04e6 --- /dev/null +++ b/test/testfiles/obj/rectangle_with_texture.obj @@ -0,0 +1,15 @@ +mtllib rectangle_with_texture.mtl + +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 1.0 1.0 0.0 +v 0.0 1.0 0.0 +vn 0.0 0.0 1.0 +vt 0.0 0.0 +vt 1.0 0.0 +vt 1.0 1.0 +vt 0.0 1.0 + +o Mesh +usemtl Material +f 1/1/1 2/2/1 3/3/1 4/4/1 diff --git a/test/testfiles/obj/texture.png b/test/testfiles/obj/texture.png new file mode 100644 index 0000000..29ce540 Binary files /dev/null and b/test/testfiles/obj/texture.png differ diff --git a/test/testfiles/obj/two_materials_same_texture.mtl b/test/testfiles/obj/two_materials_same_texture.mtl new file mode 100644 index 0000000..65a3b62 --- /dev/null +++ b/test/testfiles/obj/two_materials_same_texture.mtl @@ -0,0 +1,11 @@ +newmtl Material1 +Ka 0.000000 0.000000 0.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.000000 0.000000 0.000000 +map_Kd ./texture.png + +newmtl Material2 +Ka 0.000000 0.000000 0.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.000000 0.000000 0.000000 +map_Kd texture.png diff --git a/test/testfiles/obj/two_materials_same_texture.obj b/test/testfiles/obj/two_materials_same_texture.obj new file mode 100644 index 0000000..9b26115 --- /dev/null +++ b/test/testfiles/obj/two_materials_same_texture.obj @@ -0,0 +1,17 @@ +mtllib two_materials_same_texture.mtl + +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 1.0 1.0 0.0 +v 0.0 1.0 0.0 +vn 0.0 0.0 1.0 +vt 0.0 0.0 +vt 1.0 0.0 +vt 1.0 1.0 +vt 0.0 1.0 + +o Mesh +usemtl Material1 +f 1/1/1 2/2/1 3/3/1 +usemtl Material2 +f 1/1/1 3/3/1 4/4/1 diff --git a/test/testfiles/wrong.ext b/test/testfiles/wrong.ext new file mode 100644 index 0000000..86f9fe1 --- /dev/null +++ b/test/testfiles/wrong.ext @@ -0,0 +1 @@ +file with not importable extension diff --git a/test/tests/exporter_test.js b/test/tests/exporter_test.js index 3a1b44d..1d993f7 100644 --- a/test/tests/exporter_test.js +++ b/test/tests/exporter_test.js @@ -12,12 +12,15 @@ function CreateTestModel () material1.diffuseMap = new OV.TextureMap (); material1.diffuseMap.name = 'textures/texture1.png'; material1.diffuseMap.url = 'texture1_url'; + material1.diffuseMap.buffer = new ArrayBuffer (4); material1.specularMap = new OV.TextureMap (); material1.specularMap.name = 'textures/texture2.png'; material1.specularMap.url = 'texture2_url'; + material1.specularMap.buffer = new ArrayBuffer (4); material1.bumpMap = new OV.TextureMap (); material1.bumpMap.name = 'textures/texture3.png'; material1.bumpMap.url = 'texture3_url'; + material1.bumpMap.buffer = new ArrayBuffer (4); model.AddMaterial (material1); let material2 = new OV.Material (); @@ -64,55 +67,57 @@ describe ('Exporter', function () { let model = CreateTestModel (); let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Text, 'obj', null); + let result = exporter.Export (model, OV.FileFormat.Text, 'obj'); assert.strictEqual (result.length, 5); let mtlFile = result[0]; assert.strictEqual (mtlFile.GetName (), 'model.mtl'); assert.strictEqual (mtlFile.GetContent (), -`newmtl TestMaterial1 -Ka 0 0 0 -Kd 1 0 0 -Ks 0.2 0.2 0.2 -Ns 0 -d 1 -map_Kd texture1.png -map_Ks texture2.png -bump texture3.png -newmtl TestMaterial2 -Ka 0 0 0 -Kd 0 1 0 -Ks 0.2 0.2 0.2 -Ns 0 -d 1 -` - ); + [ + 'newmtl TestMaterial1', + 'Ka 0 0 0', + 'Kd 1 0 0', + 'Ks 0.2 0.2 0.2', + 'Ns 0', + 'd 1', + 'map_Kd texture1.png', + 'map_Ks texture2.png', + 'bump texture3.png', + 'newmtl TestMaterial2', + 'Ka 0 0 0', + 'Kd 0 1 0', + 'Ks 0.2 0.2 0.2', + 'Ns 0', + 'd 1', + '' + ].join ('\n')); let objFile = result[1]; assert.strictEqual (objFile.GetName (), 'model.obj'); assert.strictEqual (objFile.GetContent (), -`mtllib model.mtl -g TestMesh1 -v 0 0 1 -v 1 0 1 -v 0 1 1 -vn 0 0 1 -usemtl TestMaterial1 -f 1//1 2//1 3//1 -g TestMesh2 -v 0 0 0 -v 1 0 0 -v 0 1 0 -v -1 0 0 -v 0 0 1 -vn 0 0 1 -vn 0 0 1 -vn -1 0 0 -f 4//2 5//2 6//2 -usemtl TestMaterial2 -f 4//3 6//3 7//3 -f 4//4 8//4 6//4 -` - ); + [ + 'mtllib model.mtl', + 'g TestMesh1', + 'v 0 0 1', + 'v 1 0 1', + 'v 0 1 1', + 'vn 0 0 1', + 'usemtl TestMaterial1', + 'f 1//1 2//1 3//1', + 'g TestMesh2', + 'v 0 0 0', + 'v 1 0 0', + 'v 0 1 0', + 'v -1 0 0', + 'v 0 0 1', + 'vn 0 0 1', + 'vn 0 0 1', + 'vn -1 0 0', + 'f 4//2 5//2 6//2', + 'usemtl TestMaterial2', + 'f 4//3 6//3 7//3', + 'f 4//4 8//4 6//4', + '' + ].join ('\n')); let textureFile1 = result[2]; assert.strictEqual (textureFile1.GetName (), 'texture1.png'); @@ -134,51 +139,52 @@ f 4//4 8//4 6//4 let model = CreateTestModel (); let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Text, 'stl', null); + let result = exporter.Export (model, OV.FileFormat.Text, 'stl'); assert.strictEqual (result.length, 1); let stlFile = result[0]; assert.strictEqual (stlFile.GetName (), 'model.stl'); assert.strictEqual (stlFile.GetContent (), -`solid Model -facet normal 0 0 1 - outer loop - vertex 0 0 1 - vertex 1 0 1 - vertex 0 1 1 - endloop -endfacet -facet normal 0 0 1 - outer loop - vertex 0 0 0 - vertex 1 0 0 - vertex 0 1 0 - endloop -endfacet -facet normal 0 0 1 - outer loop - vertex 0 0 0 - vertex 0 1 0 - vertex -1 0 0 - endloop -endfacet -facet normal -1 0 0 - outer loop - vertex 0 0 0 - vertex 0 0 1 - vertex 0 1 0 - endloop -endfacet -endsolid Model -` - ); - }); + [ + 'solid Model', + 'facet normal 0 0 1', + ' outer loop', + ' vertex 0 0 1', + ' vertex 1 0 1', + ' vertex 0 1 1', + ' endloop', + 'endfacet', + 'facet normal 0 0 1', + ' outer loop', + ' vertex 0 0 0', + ' vertex 1 0 0', + ' vertex 0 1 0', + ' endloop', + 'endfacet', + 'facet normal 0 0 1', + ' outer loop', + ' vertex 0 0 0', + ' vertex 0 1 0', + ' vertex -1 0 0', + ' endloop', + 'endfacet', + 'facet normal -1 0 0', + ' outer loop', + ' vertex 0 0 0', + ' vertex 0 0 1', + ' vertex 0 1 0', + ' endloop', + 'endfacet', + 'endsolid Model', + '' + ].join ('\n')); + }); it ('Stl Binary Export', function () { let model = CreateTestModel (); let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Binary, 'stl', null); + let result = exporter.Export (model, OV.FileFormat.Binary, 'stl'); assert.strictEqual (result.length, 1); let stlFile = result[0]; @@ -195,76 +201,78 @@ endsolid Model let importedModel = importer.GetModel (); assert.strictEqual (importedModel.VertexCount (), 12); assert.strictEqual (importedModel.TriangleCount (), 4); - }); + }); it ('Off Export', function () { - let model = CreateTestModel (); - let exporter = new OV.Exporter (); + let model = CreateTestModel (); + let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Text, 'off', null); - assert.strictEqual (result.length, 1); + let result = exporter.Export (model, OV.FileFormat.Text, 'off'); + assert.strictEqual (result.length, 1); - let offFile = result[0]; - assert.strictEqual (offFile.GetName (), 'model.off'); - assert.strictEqual (offFile.GetContent (), -`OFF -8 4 0 -0 0 1 -1 0 1 -0 1 1 -0 0 0 -1 0 0 -0 1 0 --1 0 0 -0 0 1 -3 0 1 2 -3 3 4 5 -3 3 5 6 -3 3 7 5 -` - ); + let offFile = result[0]; + assert.strictEqual (offFile.GetName (), 'model.off'); + assert.strictEqual (offFile.GetContent (), + [ + 'OFF', + '8 4 0', + '0 0 1', + '1 0 1', + '0 1 1', + '0 0 0', + '1 0 0', + '0 1 0', + '-1 0 0', + '0 0 1', + '3 0 1 2', + '3 3 4 5', + '3 3 5 6', + '3 3 7 5', + '' + ].join ('\n')); }); it ('Ply Export', function () { - let model = CreateTestModel (); - let exporter = new OV.Exporter (); + let model = CreateTestModel (); + let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Text, 'ply', null); - assert.strictEqual (result.length, 1); + let result = exporter.Export (model, OV.FileFormat.Text, 'ply'); + assert.strictEqual (result.length, 1); - let plyFile = result[0]; - assert.strictEqual (plyFile.GetName (), 'model.ply'); - assert.strictEqual (plyFile.GetContent (), -`ply -format ascii 1.0 -element vertex 8 -property float x -property float y -property float z -element face 4 -property list uchar int vertex_index -end_header -0 0 1 -1 0 1 -0 1 1 -0 0 0 -1 0 0 -0 1 0 --1 0 0 -0 0 1 -3 0 1 2 -3 3 4 5 -3 3 5 6 -3 3 7 5 -` - ); + let plyFile = result[0]; + assert.strictEqual (plyFile.GetName (), 'model.ply'); + assert.strictEqual (plyFile.GetContent (), + [ + 'ply', + 'format ascii 1.0', + 'element vertex 8', + 'property float x', + 'property float y', + 'property float z', + 'element face 4', + 'property list uchar int vertex_index', + 'end_header', + '0 0 1', + '1 0 1', + '0 1 1', + '0 0 0', + '1 0 0', + '0 1 0', + '-1 0 0', + '0 0 1', + '3 0 1 2', + '3 3 4 5', + '3 3 5 6', + '3 3 7 5', + '' + ].join ('\n')); }); it ('Ply Binary Export', function () { let model = CreateTestModel (); let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Binary, 'ply', null); + let result = exporter.Export (model, OV.FileFormat.Binary, 'ply'); assert.strictEqual (result.length, 1); let plyFile = result[0]; @@ -284,55 +292,51 @@ end_header }); it ('Gltf Ascii Export', function () { - let model = CreateTestModel (); - let exporter = new OV.Exporter (); - - let result = exporter.Export (model, OV.FileFormat.Text, 'gltf', null); - assert.strictEqual (result.length, 3); - - let gltfFile = result[0]; - let binFile = result[1]; - let textureFile = result[2]; - assert.strictEqual (gltfFile.GetName (), 'model.gltf'); - assert.strictEqual (binFile.GetName (), 'model.bin'); - - assert.strictEqual (textureFile.GetName (), 'texture1.png'); - assert.strictEqual (textureFile.GetUrl (), 'texture1_url'); - assert.strictEqual (textureFile.GetContent (), null); - - let contentBuffer = gltfFile.GetContent (); - let importer = new OV.ImporterGltf (); - importer.Import (contentBuffer, 'gltf', { - getDefaultMaterial () { - return new OV.Material (); - }, - getFileContent (filePath) { - return binFile.GetContent (); - }, - getTextureObjectUrl (filePath) { - return null; - } - }); - - let importedModel = importer.GetModel (); - assert (OV.CheckModel (importedModel)); - assert.strictEqual (importedModel.MaterialCount (), 2); - assert.strictEqual (importedModel.MeshCount (), 2); - assert.strictEqual (importedModel.GetMesh (0).GetName (), 'TestMesh1'); - assert.strictEqual (importedModel.GetMesh (1).GetName (), 'TestMesh2'); - assert.strictEqual (importedModel.VertexCount (), 12); - assert.strictEqual (importedModel.TriangleCount (), 4); + let model = CreateTestModel (); + let exporter = new OV.Exporter (); + + let result = exporter.Export (model, OV.FileFormat.Text, 'gltf'); + assert.strictEqual (result.length, 3); + + let gltfFile = result[0]; + let binFile = result[1]; + let textureFile = result[2]; + assert.strictEqual (gltfFile.GetName (), 'model.gltf'); + assert.strictEqual (binFile.GetName (), 'model.bin'); + + assert.strictEqual (textureFile.GetName (), 'texture1.png'); + assert.strictEqual (textureFile.GetUrl (), 'texture1_url'); + assert.strictEqual (textureFile.GetContent (), null); + + let contentBuffer = gltfFile.GetContent (); + let importer = new OV.ImporterGltf (); + importer.Import (contentBuffer, 'gltf', { + getDefaultMaterial () { + return new OV.Material (); + }, + getFileBuffer (filePath) { + if (filePath == 'model.bin') { + return binFile.GetContent (); + } + return null; + } + }); + + let importedModel = importer.GetModel (); + assert (OV.CheckModel (importedModel)); + assert.strictEqual (importedModel.MaterialCount (), 2); + assert.strictEqual (importedModel.MeshCount (), 2); + assert.strictEqual (importedModel.GetMesh (0).GetName (), 'TestMesh1'); + assert.strictEqual (importedModel.GetMesh (1).GetName (), 'TestMesh2'); + assert.strictEqual (importedModel.VertexCount (), 12); + assert.strictEqual (importedModel.TriangleCount (), 4); }); it ('Gltf Binary Export', function () { let model = CreateTestModel (); let exporter = new OV.Exporter (); - let result = exporter.Export (model, OV.FileFormat.Binary, 'glb', { - getTextureBuffer : function (name) { - return new ArrayBuffer (4); - } - }); + let result = exporter.Export (model, OV.FileFormat.Binary, 'glb'); assert.strictEqual (result.length, 1); let glbFile = result[0]; @@ -344,10 +348,7 @@ end_header getDefaultMaterial () { return new OV.Material (); }, - getFileContent (filePath) { - return binFile.GetContent (); - }, - getTextureObjectUrl (filePath) { + getFileBuffer (filePath) { return null; } }); diff --git a/test/tests/importer_test.js b/test/tests/importer_test.js new file mode 100644 index 0000000..ec26f1a --- /dev/null +++ b/test/tests/importer_test.js @@ -0,0 +1,182 @@ +var assert = require ('assert'); +var path = require ('path'); + +function ImportFilesWithImporter (importer, files, callbacks) +{ + importer.LoadFilesFromFileObjects (files, function () { + importer.Import ({ + success : function (importResult) { + callbacks.success (importer, importResult); + }, + error : function (importError) { + callbacks.error (importer, importError); + } + }); + }); +} + +function ImportFiles (files, callbacks) +{ + let importer = new OV.Importer (); + ImportFilesWithImporter (importer, files, callbacks); +} + +describe ('Importer Test', function () { + it ('Empty File List', function () { + let files = []; + ImportFiles (files, { + success : function (importer, importResult) { + assert.fail (); + }, + error : function (importer, importError) { + assert.strictEqual (importError.code, OV.ImportErrorCode.NoImportableFile); + } + }); + }); + + it ('Not existing file', function () { + let files = [ + new FileObject ('obj', 'missing.obj') + ]; + ImportFiles (files, { + success : function (importer, importResult) { + assert.fail (); + }, + error : function (importer, importError) { + assert.strictEqual (importError.code, OV.ImportErrorCode.NoImportableFile); + } + }); + }); + + it ('Not imprtable file', function () { + let files = [ + new FileObject ('', 'wrong.ext') + ]; + ImportFiles (files, { + success : function (importer, importResult) { + assert.fail (); + }, + error : function (importer, importError) { + assert.strictEqual (importError.code, OV.ImportErrorCode.NoImportableFile); + } + }); + }); + + it ('Wrong file', function () { + let files = [ + new FileObject ('3ds', 'wrong.3ds') + ]; + ImportFiles (files, { + success : function (importer, importResult) { + assert.fail (); + }, + error : function (importer, importError) { + assert.strictEqual (importError.code, OV.ImportErrorCode.ImportFailed); + } + }); + }); + + it ('Single file', function () { + let files = [ + new FileObject ('obj', 'single_triangle.obj') + ]; + ImportFiles (files, { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['single_triangle.obj']); + assert.deepStrictEqual (importResult.missingFiles, []); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }); + + it ('Missing files', function () { + let files = []; + files.push (new FileObject ('obj', 'cube_with_materials.obj')); + ImportFiles (files, { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj']); + assert.deepStrictEqual (importResult.missingFiles, ['cube_with_materials.mtl']); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + files.push (new FileObject ('obj', 'cube_with_materials.mtl')); + ImportFiles (files, { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj', 'cube_with_materials.mtl']); + assert.deepStrictEqual (importResult.missingFiles, ['cube_texture.png']); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + files.push (new FileObject ('obj', 'cube_texture.png')); + ImportFiles (files, { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj', 'cube_with_materials.mtl', 'cube_texture.png']); + assert.deepStrictEqual (importResult.missingFiles, []); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }); + + it ('Missing texture multiple times', function () { + let files = [ + new FileObject ('obj', 'two_materials_same_texture.obj'), + new FileObject ('obj', 'two_materials_same_texture.mtl'), + ]; + ImportFiles (files, { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['two_materials_same_texture.obj', 'two_materials_same_texture.mtl']); + assert.deepStrictEqual (importResult.missingFiles, ['texture.png']); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }); + + it ('Append Missing files', function () { + let theImporter = new OV.Importer (); + ImportFilesWithImporter (theImporter, [new FileObject ('obj', 'cube_with_materials.obj')], { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj']); + assert.deepStrictEqual (importResult.missingFiles, ['cube_with_materials.mtl']); + ImportFilesWithImporter (theImporter, [new FileObject ('obj', 'cube_with_materials.mtl')], { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj', 'cube_with_materials.mtl']); + assert.deepStrictEqual (importResult.missingFiles, ['cube_texture.png']); + ImportFilesWithImporter (theImporter, [new FileObject ('obj', 'cube_texture.png')], { + success : function (importer, importResult) { + assert (!OV.IsModelEmpty (importResult.model)); + assert.deepStrictEqual (importResult.usedFiles, ['cube_with_materials.obj', 'cube_with_materials.mtl', 'cube_texture.png']); + assert.deepStrictEqual (importResult.missingFiles, []); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }, + error : function (importer, importError) { + assert.fail (); + } + }); + }); +}); diff --git a/test/tests/importerobj_test.js b/test/tests/importerobj_test.js index 03cbce4..824be87 100644 --- a/test/tests/importerobj_test.js +++ b/test/tests/importerobj_test.js @@ -374,6 +374,69 @@ describe ('Obj Importer', function() { }); }); + it ('rectangle_with_texture.obj', function () { + var model = testFiles.ImportObjFile ('rectangle_with_texture.obj'); + assert (OV.CheckModel (model)); + assert.deepStrictEqual (testUtils.ModelToObject (model), { + name : '', + materials : [ + { name : 'Material' } + ], + meshes : [ + { + name : 'Mesh', + triangles : [ + { + vertices : [0, 0, 0, 1, 0, 0, 1, 1, 0], + normals : [0, 0, 1, 0, 0, 1, 0, 0, 1], + uvs : [0, 0, 1, 0, 1, 1], + mat : 0 + }, + { + vertices : [0, 0, 0, 1, 1, 0, 0, 1, 0], + normals : [0, 0, 1, 0, 0, 1, 0, 0, 1], + uvs : [0, 0, 1, 1, 0, 1], + mat : 0 + } + ] + } + ] + }); + }); + + it ('two_materials_same_texture.obj', function () { + var model = testFiles.ImportObjFile ('two_materials_same_texture.obj'); + assert.strictEqual (model.GetMaterial (0).diffuseMap.url, model.GetMaterial (1).diffuseMap.url); + assert.strictEqual (model.GetMaterial (0).diffuseMap.buffer.byteLength, model.GetMaterial (1).diffuseMap.buffer.byteLength); + assert (OV.CheckModel (model)); + assert.deepStrictEqual (testUtils.ModelToObject (model), { + name : '', + materials : [ + { name : 'Material1' }, + { name : 'Material2' } + ], + meshes : [ + { + name : 'Mesh', + triangles : [ + { + vertices : [0, 0, 0, 1, 0, 0, 1, 1, 0], + normals : [0, 0, 1, 0, 0, 1, 0, 0, 1], + uvs : [0, 0, 1, 0, 1, 1], + mat : 0 + }, + { + vertices : [0, 0, 0, 1, 1, 0, 0, 1, 0], + normals : [0, 0, 1, 0, 0, 1, 0, 0, 1], + uvs : [0, 0, 1, 1, 0, 1], + mat : 1 + } + ] + } + ] + }); + }); + it ('cube_with_materials.obj', function () { var model = testFiles.ImportObjFile ('cube_with_materials.obj'); assert (OV.CheckModel (model)); diff --git a/test/tests/io_test.js b/test/tests/io_test.js index a97d1de..4f3591c 100644 --- a/test/tests/io_test.js +++ b/test/tests/io_test.js @@ -1,9 +1,9 @@ var assert = require ('assert'); -var testFiles = require ('../utils/testfiles.js'); +var testUtils = require ('../utils/testutils.js'); describe ('IO Test', function () { it ('Binary Reader', function () { - let buffer = testFiles.GetArrayBufferFileContent ('bin', 'binary_content.bin'); + let buffer = testUtils.GetArrayBufferFileContent ('bin', 'binary_content.bin'); let reader = new OV.BinaryReader (buffer, true); assert.strictEqual (reader.GetByteLength (), 166); @@ -89,4 +89,13 @@ describe ('IO Test', function () { assert.strictEqual (reader.ReadFloat32 (), 7.5); assert.strictEqual (reader.ReadDouble64 (), 8.5); }); + + it ('Utf8 Conversion', function () { + let str = 'example-\u2764-example'; + let buffer = OV.Utf8StringToArrayBuffer (str); + assert.strictEqual (buffer.byteLength, 19); + let str2 = OV.ArrayBufferToUtf8String (buffer); + assert.strictEqual (str, str2); + assert.strictEqual (str.length, str2.length); + }); }); diff --git a/test/utils/globals.js b/test/utils/globals.js new file mode 100644 index 0000000..4f66e2c --- /dev/null +++ b/test/utils/globals.js @@ -0,0 +1,60 @@ +var path = require ('path'); +var testUtils = require ('./testutils.js'); + +global.atob = function (base64String) { + return Buffer.from (base64String, 'base64').toString('binary'); +}; + +global.Blob = function () { + +}; + +var objectUrlCounter = 0; +global.URL = { + createObjectURL : function () { + objectUrlCounter += 1; + return 'ObjectUrl:' + objectUrlCounter.toString (); + } +}; + +global.FileObject = function (folderName, fileName) +{ + this.folderName = folderName; + this.fileName = fileName; + this.name = path.join (folderName, fileName); +}; + +global.FileReader = class +{ + static DONE = 2; + + readAsText (fileObject) + { + let content = testUtils.GetTextFileContent (fileObject.folderName, fileObject.fileName); + if (content !== null) { + this.onloadend ({ + target : { + readyState : FileReader.DONE, + result : content + } + }); + } else { + this.onerror (); + } + } + + readAsArrayBuffer (fileObject) + { + let content = testUtils.GetArrayBufferFileContent (fileObject.folderName, fileObject.fileName); + if (content !== null) { + this.onloadend ({ + target : { + readyState : FileReader.DONE, + result : content + } + }); + } else { + this.onerror (); + } + } +}; diff --git a/test/utils/testfiles.js b/test/utils/testfiles.js index ed4cd2d..36272c7 100644 --- a/test/utils/testfiles.js +++ b/test/utils/testfiles.js @@ -1,28 +1,7 @@ -var path = require ('path'); -var fs = require ('fs'); -var testUtils = require ('./testutils.js') +var testUtils = require ('./testutils.js'); module.exports = { - GetTextFileContent : function (folder, fileName) - { - var testFilePath = path.join ('testfiles', folder, fileName); - return fs.readFileSync (testFilePath).toString (); - }, - - GetArrayBufferFileContent : function (folder, fileName) - { - var testFilePath = path.join ('testfiles', folder, fileName); - var buffer = fs.readFileSync (testFilePath); - var arrayBuffer = new ArrayBuffer (buffer.length); - var uint8Array = new Uint8Array (arrayBuffer); - var i; - for (i = 0; i < buffer.length; ++i) { - uint8Array[i] = buffer[i]; - } - return arrayBuffer - }, - ImportObjFile : function (fileName) { var importer = new OV.ImporterObj (); @@ -65,13 +44,30 @@ module.exports = }, ImportFile : function (importer, format, folder, fileName) - { - var myThis = this; + { + function GetFileBuffer (filePath, importer) + { + let extension = OV.GetFileExtension (filePath); + let knownFormats = importer.GetKnownFileFormats (); + let format = OV.FileFormat.Binary; + if (knownFormats[extension] !== undefined) { + format = knownFormats[extension]; + } + let fileContent = null; + if (format == OV.FileFormat.Text) { + fileContent = testUtils.GetTextFileContent (folder, filePath); + } else if (format == OV.FileFormat.Binary) { + fileContent = testUtils.GetArrayBufferFileContent (folder, filePath); + } + return fileContent; + } + + var obj = this; var content = null; if (format == OV.FileFormat.Text) { - content = this.GetTextFileContent (folder, fileName); + content = testUtils.GetTextFileContent (folder, fileName); } else if (format == OV.FileFormat.Binary) { - content = this.GetArrayBufferFileContent (folder, fileName); + content = testUtils.GetArrayBufferFileContent (folder, fileName); } var extension = OV.GetFileExtension (fileName); importer.Import (content, extension, { @@ -79,23 +75,8 @@ module.exports = var material = new OV.Material (); return material; }, - getFileContent : function (requestedFileName) { - let extension = OV.GetFileExtension (requestedFileName); - let knownFormats = importer.GetKnownFileFormats (); - let format = OV.FileFormat.Binary; - if (knownFormats[extension] !== undefined) { - format = knownFormats[extension]; - } - let fileContent = null; - if (format == OV.FileFormat.Text) { - fileContent = myThis.GetTextFileContent (folder, requestedFileName); - } else if (format == OV.FileFormat.Binary) { - fileContent = myThis.GetArrayBufferFileContent (folder, requestedFileName); - } - return fileContent; - }, - getTextureObjectUrl : function (requestedFileName) { - return requestedFileName; + getFileBuffer : function (filePath) { + return GetFileBuffer (filePath, importer); } }); let model = importer.GetModel (); diff --git a/test/utils/testutils.js b/test/utils/testutils.js index cd23992..83f3fb4 100644 --- a/test/utils/testutils.js +++ b/test/utils/testutils.js @@ -1,5 +1,30 @@ +var fs = require ('fs'); +var path = require ('path'); + module.exports = { + GetTextFileContent : function (folder, fileName) + { + var testFilePath = path.join ('testfiles', folder, fileName); + if (!fs.existsSync (testFilePath)) { + return null; + } + return fs.readFileSync (testFilePath).toString (); + }, + + GetArrayBufferFileContent : function (folder, fileName) + { + var testFilePath = path.join ('testfiles', folder, fileName); + var buffer = fs.readFileSync (testFilePath); + var arrayBuffer = new ArrayBuffer (buffer.length); + var uint8Array = new Uint8Array (arrayBuffer); + var i; + for (i = 0; i < buffer.length; ++i) { + uint8Array[i] = buffer[i]; + } + return arrayBuffer + }, + ModelToObject : function (model) { var obj = { diff --git a/tools/create_package.py b/tools/create_package.py index c1e39fd..28678f1 100644 --- a/tools/create_package.py +++ b/tools/create_package.py @@ -37,7 +37,7 @@ def CompressFiles (inputFiles, outputFile): return False return True -def CreateDestinationDir (config, rootDir, websiteDir, version): +def CreateDestinationDir (config, rootDir, websiteDir, version, testBuild): if not os.path.exists (websiteDir): os.makedirs (websiteDir) @@ -71,7 +71,7 @@ def CreateDestinationDir (config, rootDir, websiteDir, version): metaContent = Tools.GetFileContent (metaFile) replacer.ReplaceTokenContent ('', '', metaContent) analyticsFile = os.path.join (rootDir, 'tools', 'website_analytics_data.txt') - if os.path.exists (analyticsFile): + if os.path.exists (analyticsFile) and not testBuild: analyticsContent = Tools.GetFileContent (analyticsFile) replacer.ReplaceTokenContent ('', '', analyticsContent) scriptFile = os.path.join (rootDir, 'tools', 'website_script_data.txt') @@ -86,8 +86,8 @@ def CreatePackage (websiteDir, packageDir, version): zipPath = os.path.join (packageDir, 'o3dv_' + version + '.zip') zip = zipfile.ZipFile (zipPath, mode = 'w', compression = zipfile.ZIP_DEFLATED) - zip.write (os.path.join (websiteDir, 'libs', 'three.min-126.js'), 'three.min.js') - zip.write (os.path.join (websiteDir, 'o3dv', 'o3dv.min.js'), 'o3dv.min.js') + zip.write (os.path.join (websiteDir, 'libs', 'three.min-126.js'), 'three.min-126.js') + zip.write (os.path.join (websiteDir, 'o3dv', 'o3dv.min.js'), 'o3dv.min-' + version + '.js') zip.close () return True @@ -96,12 +96,19 @@ def Main (argv): os.chdir (currentDir) rootDir = os.path.abspath ('..') - buildDir = os.path.join (rootDir, 'build') + + testBuild = False + buildDir = os.path.join (rootDir, 'build', 'final') + if len (argv) >= 2 and argv[1] == 'test': + testBuild = True + buildDir = os.path.join (rootDir, 'build', 'test') + PrintInfo ('Creating test build.') + websiteDir = os.path.join (buildDir, 'website') packageDir = os.path.join (buildDir, 'package') if os.path.exists (buildDir): shutil.rmtree (buildDir) - + config = None with open ('config.json') as configJson: config = json.load (configJson) @@ -120,7 +127,7 @@ def Main (argv): version = GetVersion (rootDir) PrintInfo ('Create build directory') - CreateDestinationDir (config, rootDir, websiteDir, version) + CreateDestinationDir (config, rootDir, websiteDir, version, testBuild) PrintInfo ('Compress importer sources.') compressResult = CompressFiles (config['importer_files'], os.path.join (websiteDir, 'o3dv', 'o3dv.min.js')) diff --git a/website/embed_iframe.html b/tools/sandbox/embed_iframe.html similarity index 56% rename from website/embed_iframe.html rename to tools/sandbox/embed_iframe.html index 2c4258b..7841e2c 100644 --- a/website/embed_iframe.html +++ b/tools/sandbox/embed_iframe.html @@ -11,22 +11,22 @@ diff --git a/tools/sandbox/embed_selfhost.html b/tools/sandbox/embed_selfhost.html new file mode 100644 index 0000000..5c435f0 --- /dev/null +++ b/tools/sandbox/embed_selfhost.html @@ -0,0 +1,37 @@ + + + + + + + + + Online 3D Viewer + + + + + + + + + + +
+
+
+
+
+ + + diff --git a/tools/generate_test_obj.py b/tools/sandbox/generate_test_obj.py similarity index 100% rename from tools/generate_test_obj.py rename to tools/sandbox/generate_test_obj.py diff --git a/tools/sandbox/index.html b/tools/sandbox/three_minimal.html similarity index 96% rename from tools/sandbox/index.html rename to tools/sandbox/three_minimal.html index d85c8a6..e3c99f3 100644 --- a/tools/sandbox/index.html +++ b/tools/sandbox/three_minimal.html @@ -11,7 +11,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - diff --git a/website/info/index.html b/website/info/index.html index f9affce..d8f97bc 100644 --- a/website/info/index.html +++ b/website/info/index.html @@ -47,8 +47,8 @@

Supported file formats

- The website supports several file formats for import and export. If a file format has an ascii and binary version, - usually it's recommended to use the binary version. During export an ascii file is created. + The website supports several file formats for import and export. If a file format has text and binary version, + usually it's recommended to use the binary version.

@@ -60,7 +60,7 @@ - + @@ -74,7 +74,7 @@ - + @@ -87,7 +87,7 @@ - + @@ -100,20 +100,20 @@ - + + - - + - + diff --git a/website/info/info.css b/website/info/info.css index 191aa60..f985fb8 100644 --- a/website/info/info.css +++ b/website/info/info.css @@ -118,13 +118,13 @@ table td.center table td.green { - color: #21C23C; + color: #21c23c; font-weight: bold; } table td.red { - color: #BF233D; + color: #bf233d; font-weight: bold; } @@ -141,6 +141,8 @@ div.code font-size: 14px; margin: 10px 0px; padding : 5px; + overflow: auto; + white-space: nowrap; } @media only screen and (max-width: 1000px) diff --git a/website/o3dv/dialogs.js b/website/o3dv/dialogs.js index 56af225..336b05f 100644 --- a/website/o3dv/dialogs.js +++ b/website/o3dv/dialogs.js @@ -54,7 +54,7 @@ OV.ButtonDialog = class let contentDiv = this.modal.GetContentDiv (); contentDiv.addClass ('ov_dialog'); - let titleDiv = $('
').addClass ('ov_dialog_title').html (title).appendTo (contentDiv); + $('
').addClass ('ov_dialog_title').html (title).appendTo (contentDiv); let dialogContentDiv = $('
').addClass ('ov_dialog_content').appendTo (contentDiv); let buttonsDiv = $('
').addClass ('ov_dialog_buttons').appendTo (contentDiv); let buttonsInnerDiv = $('
').addClass ('ov_dialog_buttons_inner').appendTo (buttonsDiv); @@ -65,6 +65,11 @@ OV.ButtonDialog = class return dialogContentDiv; } + SetCloseHandler (closeHandler) + { + this.modal.SetCloseHandler (closeHandler); + } + Show () { this.modal.Open (); @@ -152,14 +157,14 @@ OV.ShowOpenUrlDialog = function (onOk) { for (let i = 0; i < urls.length; i++) { let url = urls[i]; - if (url.search ('www.dropbox.com') !== -1) { + if (url.search (/www\.dropbox\.com/u) !== -1) { url = url.replace ('www.dropbox.com', 'dl.dropbox.com'); let separatorPos = url.indexOf ('?'); if (separatorPos !== -1) { url = url.substr (0, separatorPos); } urls[i] = url; - } else if (url.search ('github.com') !== -1) { + } else if (url.search (/github\.com/u) !== -1) { url = url.replace ('github.com', 'raw.githubusercontent.com'); url = url.replace ('/blob', ''); let separatorPos = url.indexOf ('?'); @@ -201,7 +206,7 @@ OV.ShowOpenUrlDialog = function (onOk) return dialog; }; -OV.ShowExportDialog = function (model, importer) +OV.ShowExportDialog = function (model) { if (model === null) { return OV.ShowMessageDialog ('Export Failed', 'Please load a model to export', null); @@ -221,12 +226,14 @@ OV.ShowExportDialog = function (model, importer) $('
').html (text).addClass ('ov_dialog_section').appendTo (contentDiv); let formats = [ - { name : 'obj (ascii)', format : OV.FileFormat.Text, extension : 'obj' }, - { name : 'stl (ascii)', format : OV.FileFormat.Text, extension : 'stl' }, + { name : 'obj (text)', format : OV.FileFormat.Text, extension : 'obj' }, + { name : 'stl (text)', format : OV.FileFormat.Text, extension : 'stl' }, { name : 'stl (binary)', format : OV.FileFormat.Binary, extension : 'stl' }, - { name : 'ply (ascii)', format : OV.FileFormat.Text, extension : 'ply' }, + { name : 'ply (text)', format : OV.FileFormat.Text, extension : 'ply' }, { name : 'ply (binary)', format : OV.FileFormat.Binary, extension : 'ply' }, - { name : 'off (ascii)', format : OV.FileFormat.Text, extension : 'off' } + { name : 'gltf (text)', format : OV.FileFormat.Text, extension : 'gltf' }, + { name : 'gltf (binary)', format : OV.FileFormat.Binary, extension : 'glb' }, + { name : 'off (text)', format : OV.FileFormat.Text, extension : 'off' } ]; let formatSelect = $('
objasciitext with mtl and texture support
stlasciitext binary version is preferred
plyasciitext binary version is preferred
gltfascii (.gltf)text (.gltf) version 2.0
binary (.glb) version 2.0
offasciitext -