1 /*
  2  * ModelLoader.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2017 Emmanuel PUYBARET / eTeks <info@eteks.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