1 /* 2 * ModelLoader.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 21 // Requires core.js 22 // jszip.min.js 23 // scene3d.js 24 // URLContent.js 25 26 /** 27 * Creates an instance of a model loader. 28 * @param {string} modelExtension 29 * @constructor 30 * @author Emmanuel Puybaret 31 */ 32 function ModelLoader(modelExtension) { 33 this.modelExtension = modelExtension; 34 this.parserBusy = false; 35 this.waitingParsedEntries = []; 36 } 37 38 // Constants used to follow model loading progression (moved from Node3D) 39 ModelLoader.READING_MODEL = Node3D.READING_MODEL; 40 ModelLoader.PARSING_MODEL = Node3D.PARSING_MODEL; 41 ModelLoader.BUILDING_MODEL = Node3D.BUILDING_MODEL; 42 ModelLoader.BINDING_MODEL = Node3D.BINDING_MODEL; 43 44 /** 45 * Loads the 3D model from the given URL. This method is reentrant when run asynchronously. 46 * @param {string} url The URL of a zip file containing an entry with the extension given in constructor 47 * that will be loaded or an URL noted as jar:url!/modelEntry where modelEntry will be loaded. 48 * @param {boolean} [synchronous] optional parameter equal to false by default 49 * @param {{modelLoaded, modelError, progression}} loadingModelObserver 50 * the observer containing <code>modelLoaded(model)</code>, <code>modelError(error)</code>, 51 * <code>progression(part, info, percentage)</code> methods that will be called at various phases, 52 * with <code>model<code> being an instance of <code>Node3D</code>, 53 * <code>error</code>, <code>part</code>, <code>info</code> strings 54 * and <code>percentage</code> a number. 55 */ 56 ModelLoader.prototype.load = function(url, synchronous, loadingModelObserver) { 57 if (loadingModelObserver === undefined) { 58 loadingModelObserver = synchronous; 59 synchronous = false; 60 } 61 var modelEntryName = null; 62 if (url.indexOf("jar:") === 0) { 63 var entrySeparatorIndex = url.indexOf("!/"); 64 modelEntryName = url.substring(entrySeparatorIndex + 2); 65 url = url.substring(4, entrySeparatorIndex); 66 } 67 68 loadingModelObserver.progression(ModelLoader.READING_MODEL, url, 0); 69 var loader = this; 70 var zipObserver = { 71 zipReady : function(zip) { 72 try { 73 if (modelEntryName === null) { 74 // Search an entry ending with the given extension 75 var entries = zip.file(/.*/); 76 for (var i = 0; i < entries.length; i++) { 77 if (entries [i].name.toLowerCase().match(new RegExp("\." + loader.modelExtension.toLowerCase() + "$"))) { 78 loader.parseModelEntry(entries [i], zip, url, synchronous, loadingModelObserver); 79 return; 80 } 81 } 82 if (entries.length > 0) { 83 // If not found, try with the first entry 84 modelEntryName = entries [0].name; 85 } else { 86 if (loadingModelObserver.modelError !== undefined) { 87 loadingModelObserver.modelError("Empty file"); 88 } 89 return; 90 } 91 } 92 loader.parseModelEntry(zip.file(decodeURIComponent(modelEntryName)), zip, url, synchronous, loadingModelObserver); 93 } catch (ex) { 94 zipObserver.zipError(ex); 95 } 96 }, 97 zipError : function(error) { 98 if (loadingModelObserver.modelError !== undefined) { 99 loadingModelObserver.modelError(error); 100 } 101 }, 102 progression : function(part, info, percentage) { 103 if (loadingModelObserver.progression !== undefined) { 104 loadingModelObserver.progression(ModelLoader.READING_MODEL, info, percentage); 105 } 106 } 107 }; 108 ZIPTools.getZIP(url, synchronous, zipObserver); 109 } 110 111 /** 112 * Clears the list of 3D models waiting to be parsed by this loader. 113 */ 114 ModelLoader.prototype.clear = function() { 115 this.waitingParsedEntries = []; 116 } 117 118 /** 119 * Parses the content of the given entry to create the scene it contains. 120 * @private 121 */ 122 ModelLoader.prototype.parseModelEntry = function(modelEntry, zip, zipUrl, synchronous, loadingModelObserver) { 123 if (synchronous) { 124 var modelContent = this.getModelContent(modelEntry); 125 loadingModelObserver.progression(ModelLoader.READING_MODEL, modelEntry.name, 1); 126 var modelContext = {}; 127 this.parseDependencies(modelContent, modelEntry.name, zip, modelContext); 128 var scene = this.parseEntryScene(modelContent, modelEntry.name, zip, modelContext, null, loadingModelObserver.progression); 129 this.loadTextureImages(scene, {}, zip, zipUrl, true); 130 loadingModelObserver.modelLoaded(scene); 131 } else { 132 var parsedEntry = {modelEntry : modelEntry, 133 zip : zip, 134 zipUrl : zipUrl, 135 loadingModelObserver : loadingModelObserver}; 136 this.waitingParsedEntries.push(parsedEntry); 137 this.parseNextWaitingEntry(); 138 } 139 } 140 141 /** 142 * Parses asynchronously the waiting entries. 143 * @private 144 */ 145 ModelLoader.prototype.parseNextWaitingEntry = function() { 146 if (!this.parserBusy) { 147 // Parse model files one at a time to avoid keeping in memory unzipped content not yet used 148 for (var key in this.waitingParsedEntries) { 149 if (this.waitingParsedEntries.hasOwnProperty(key)) { 150 var parsedEntry = this.waitingParsedEntries [key]; 151 var modelEntryName = parsedEntry.modelEntry.name; 152 // Get model content to parse 153 var modelContent = this.getModelContent(parsedEntry.modelEntry); 154 parsedEntry.loadingModelObserver.progression(ModelLoader.READING_MODEL, modelEntryName, 1); 155 var modelContext = {}; 156 this.parseDependencies(modelContent, modelEntryName, parsedEntry.zip, modelContext); 157 var loader = this; 158 // Post future work (avoid worker because the amount of data to transfer back and forth slows the program) 159 setTimeout( 160 function() { 161 loader.parseEntryScene(modelContent, modelEntryName, parsedEntry.zip, modelContext, 162 function(scene) { 163 loader.loadTextureImages(scene, {}, parsedEntry.zip, parsedEntry.zipUrl, true); 164 parsedEntry.loadingModelObserver.modelLoaded(scene); 165 loader.parserBusy = false; 166 loader.parseNextWaitingEntry(); 167 }, 168 parsedEntry.loadingModelObserver.progression); 169 }, 0); 170 171 this.parserBusy = true; 172 // Remove parsed entry from waiting list 173 delete this.waitingParsedEntries [key]; 174 break; 175 } 176 } 177 } 178 } 179 180 /** 181 * Loads the textures images used by appearances of the scene. 182 * @private 183 */ 184 ModelLoader.prototype.loadTextureImages = function(node, images, zip, zipUrl, synchronous) { 185 if (node instanceof Group3D) { 186 for (var i = 0; i < node.children.length; i++) { 187 this.loadTextureImages(node.children [i], images, zip, zipUrl, synchronous); 188 } 189 } else if (node instanceof Link3D) { 190 this.loadTextureImages(node.getSharedGroup(), images, zip, zipUrl, synchronous); 191 } else if (node instanceof Shape3D) { 192 var appearance = node.getAppearance(); 193 if (appearance) { 194 var imageEntryName = appearance.imageEntryName; 195 if (imageEntryName !== undefined) { 196 delete appearance [imageEntryName]; 197 if (imageEntryName in images) { 198 appearance.setTextureImage(images [imageEntryName]); 199 } else { 200 var image = new Image(); 201 image.crossOrigin = "anonymous"; 202 appearance.setTextureImage(image); 203 image.url = "jar:" + zipUrl + "!/" + imageEntryName; 204 // Store loaded image to avoid duplicates 205 images [imageEntryName] = image; 206 207 var loader = function() { 208 var imageEntry = zip.file(decodeURIComponent(imageEntryName)); 209 if (imageEntry !== null) { 210 var imageData = imageEntry.asBinary(); 211 var base64Image = btoa(imageData); 212 var extension = imageEntryName.substring(imageEntryName.lastIndexOf('.') + 1).toLowerCase(); 213 var mimeType = ZIPTools.isJPEGImage(imageData) 214 ? "image/jpeg" 215 : (ZIPTools.isPNGImage(imageData) 216 ? "image/png" 217 : ("image/" + extension)); 218 // Detect quickly if a PNG image use transparency 219 image.transparent = ZIPTools.isTransparentImage(imageData); 220 image.src = "data:" + mimeType + ";base64," + base64Image; 221 } else { 222 appearance.setTextureImage(null); 223 } 224 }; 225 if (synchronous) { 226 loader(); 227 } else { 228 setTimeout(loader, 0); 229 } 230 } 231 } 232 } 233 } 234 } 235 236 /** 237 * Returns the content of the model stored in the given entry. 238 * @protected 239 */ 240 ModelLoader.prototype.getModelContent = function(modelEntry) { 241 return modelEntry.asBinary(); 242 } 243 244 /** 245 * Parses the dependencies of the model content if any and returns the materials it describes. 246 * @protected 247 */ 248 ModelLoader.prototype.parseDependencies = function(modelContent, modelEntryName, zip, modelContext) { 249 } 250 251 /** 252 * Parses the given model content and calls onmodelloaded asynchronously or 253 * returns the scene it describes if onmodelloaded is null. 254 * @protected 255 */ 256 ModelLoader.prototype.parseEntryScene = function(modelContent, modelEntryName, zip, modelContext, onmodelloaded, onprogression) { 257 } 258