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}} modelObserver 
 50  *            An observer containing modelLoaded(model), 
 51  *            modelError(error), progression(part, info, percentage) methods that
 52  *            will called at various phases.
 53  */
 54 ModelLoader.prototype.load = function(url, synchronous, modelObserver) {
 55   if (modelObserver === undefined) {
 56     modelObserver = synchronous;
 57     synchronous = false;
 58   }
 59   var modelEntryName = null;
 60   if (url.indexOf("jar:") === 0) {
 61     var entrySeparatorIndex = url.indexOf("!/");
 62     modelEntryName = url.substring(entrySeparatorIndex + 2);
 63     url = url.substring(4, entrySeparatorIndex);
 64   }
 65   
 66   modelObserver.progression(ModelLoader.READING_MODEL, url, 0);
 67   var loader = this;
 68   var zipObserver = {
 69       zipReady : function(zip) {
 70         try {
 71           if (modelEntryName === null) {
 72             // Search an entry ending with the given extension
 73             var entries = zip.file(/.*/);
 74             for (var i = 0; i < entries.length; i++) {
 75               if (entries [i].name.toLowerCase().match(new RegExp("\." + loader.modelExtension.toLowerCase() + "$"))) {
 76                 loader.parseModelEntry(entries [i], zip, url, synchronous, modelObserver);
 77                 return;
 78               } 
 79             }
 80             if (entries.length > 0) {
 81               // If not found, try with the first entry
 82               modelEntryName = entries [0].name;
 83             } else {
 84               if (modelObserver.modelError !== undefined) {
 85                 modelObserver.modelError("Empty file");
 86               }
 87               return;
 88             }
 89           }
 90           loader.parseModelEntry(zip.file(decodeURIComponent(modelEntryName)), zip, url, synchronous, modelObserver);
 91         } catch (ex) {
 92           zipObserver.zipError(ex);
 93         }
 94       },
 95       zipError : function(error) {
 96         if (modelObserver.modelError !== undefined) {
 97           modelObserver.modelError(error);
 98         }
 99       },
100       progression : function(part, info, percentage) {
101         if (modelObserver.progression !== undefined) {
102           modelObserver.progression(ModelLoader.READING_MODEL, info, percentage);
103         }
104       }
105     };
106   ZIPTools.getZIP(url, synchronous, zipObserver);
107 }
108 
109 /**
110  * Clears the list of 3D models waiting to be parsed by this loader. 
111  */
112 ModelLoader.prototype.clear = function() {
113   this.waitingParsedEntries = [];
114 }
115 
116 /**
117  * Parses the content of the given entry to create the scene it contains. 
118  * @private
119  */
120 ModelLoader.prototype.parseModelEntry = function(modelEntry, zip, zipUrl, synchronous, modelObserver) {
121   if (synchronous) { 
122     var modelContent = this.getModelContent(modelEntry);
123     modelObserver.progression(ModelLoader.READING_MODEL, modelEntry.name, 1);
124     var modelContext = {};
125     this.parseDependencies(modelContent, modelEntry.name, zip, modelContext);
126     var scene = this.parseEntryScene(modelContent, modelEntry.name, zip, modelContext, null, modelObserver.progression);
127     this.loadTextureImages(scene, {}, zip, zipUrl, synchronous);
128     modelObserver.modelLoaded(scene);
129   } else {
130     var parsedEntry = {modelEntry : modelEntry, 
131                        zip : zip, 
132                        zipUrl : zipUrl, 
133                        modelObserver : modelObserver};
134     this.waitingParsedEntries.push(parsedEntry);
135     this.parseNextWaitingEntry();
136   }
137 }  
138 
139 /**
140  * Parses asynchronously the waiting entries.
141  * @private
142  */
143 ModelLoader.prototype.parseNextWaitingEntry = function() {
144   if (!this.parserBusy) {
145     // Parse model files one at a time to avoid keeping in memory unzipped content not yet used
146     for (var key in this.waitingParsedEntries) {
147       if (this.waitingParsedEntries.hasOwnProperty(key)) {
148         var parsedEntry = this.waitingParsedEntries [key];
149         var modelEntryName = parsedEntry.modelEntry.name;
150         // Get model content to parse
151         var modelContent = this.getModelContent(parsedEntry.modelEntry);
152         parsedEntry.modelObserver.progression(ModelLoader.READING_MODEL, modelEntryName, 1);
153         var modelContext = {};
154         this.parseDependencies(modelContent, modelEntryName, parsedEntry.zip, modelContext);
155         var loader = this;
156         // Post future work (avoid worker because the amount of data to transfer back and forth slows the program) 
157         setTimeout(
158             function() {
159               loader.parseEntryScene(modelContent, modelEntryName, parsedEntry.zip, modelContext,
160                   function(scene) {
161                       loader.loadTextureImages(scene, {}, parsedEntry.zip, parsedEntry.zipUrl);
162                       parsedEntry.modelObserver.modelLoaded(scene);
163                       loader.parserBusy = false;
164                       loader.parseNextWaitingEntry();
165                     },
166                   parsedEntry.modelObserver.progression);
167             }, 0);
168         
169         this.parserBusy = true;
170         // Remove parsed entry from waiting list
171         delete this.waitingParsedEntries [key];
172         break;
173       }
174     }
175   }
176 }
177 
178 /**
179  * Loads the textures images used by appearances of the scene.
180  * @private
181  */
182 ModelLoader.prototype.loadTextureImages = function(node, images, zip, zipUrl, synchronous) {
183   if (node instanceof Group3D) {
184     for (var i = 0; i < node.children.length; i++) {
185       this.loadTextureImages(node.children [i], images, zip, zipUrl, synchronous);
186     }
187   } else if (node instanceof Link3D) {
188     this.loadTextureImages(node.getSharedGroup(), images, zip, zipUrl, synchronous);
189   } else if (node instanceof Shape3D) {
190     var appearance = node.getAppearance();
191     if (appearance) {
192       var imageEntryName = appearance.imageEntryName;
193       if (imageEntryName !== undefined) {
194         delete appearance [imageEntryName];
195         if (imageEntryName in images) {
196           appearance.setTextureImage(images [imageEntryName]);
197         } else { 
198           var image = new Image();
199           appearance.setTextureImage(image);
200           image.url = "jar:" + zipUrl + "!/" + imageEntryName;
201           // Store loaded image to avoid duplicates
202           images [imageEntryName] = image;
203           
204           var loader = function() {
205             var imageEntry = zip.file(decodeURIComponent(imageEntryName));
206             if (imageEntry !== null) {
207               var imageData = imageEntry.asBinary();
208               var base64Image = btoa(imageData);
209               var extension = imageEntryName.substring(imageEntryName.lastIndexOf('.') + 1).toLowerCase();
210               var mimeType = extension == "jpg"
211                   ? "image/jpeg" 
212                   : ("image/" + extension);
213               // Detect quickly if a PNG image use transparency
214               image.transparent = ZIPTools.isTranparentImage(imageData);
215               image.src = "data:" + mimeType + ";base64," + base64Image;
216             } else {
217               appearance.setTextureImage(null);
218             }
219           };
220           if (synchronous) {
221             loader();
222           } else {
223             setTimeout(loader, 0);
224           }
225         }
226       }
227     }
228   }
229 }
230 
231 /**
232  * Returns the content of the model stored in the given entry.
233  * @protected
234  */
235 ModelLoader.prototype.getModelContent = function(modelEntry) {
236   return modelEntry.asBinary();
237 }
238 
239 /**
240  * Parses the dependencies of the model content if any and returns the materials it describes.
241  * @protected
242  */
243 ModelLoader.prototype.parseDependencies = function(modelContent, modelEntryName, zip, modelContext) {
244 }
245 
246 /**
247  * Parses the given model content and calls onmodelloaded asynchronously or 
248  * returns the scene it describes if onmodelloaded is null.
249  * @protected
250  */
251 ModelLoader.prototype.parseEntryScene = function(modelContent, modelEntryName, zip, modelContext, onmodelloaded, onprogression) {
252 }
253