1 /*
  2  * TextureManager.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  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 URLContent.js
 22 
 23 /**
 24  * Singleton managing texture image cache.
 25  * @constructor
 26  * @author Emmanuel Puybaret
 27  */
 28 function TextureManager() {
 29   this.loadedTextureImages = {};
 30   this.loadingTextureObservers = {};
 31 }
 32 
 33 TextureManager.READING_TEXTURE = "Reading texture";
 34 
 35 // Singleton
 36 TextureManager.instance = null;
 37 
 38 /**
 39  * Returns an instance of this singleton.
 40  * @return {TextureManager} 
 41  */
 42 TextureManager.getInstance = function() {
 43   if (TextureManager.instance == null) {
 44     TextureManager.instance = new TextureManager();
 45   }
 46   return TextureManager.instance;
 47 }
 48 
 49 /**
 50  * Clears loaded texture images cache. 
 51  */
 52 TextureManager.prototype.clear = function() {
 53   this.loadedTextureImages = {};
 54   this.loadingTextureObservers = {};
 55 }
 56 
 57 
 58 /**
 59  * Reads a texture image from <code>content</code> notified to <code>textureObserver</code>. 
 60  * If the texture isn't loaded in cache yet and <code>synchronous</code> is false, a one pixel 
 61  * white image texture will be notified immediately to the given <code>textureObserver</code>, 
 62  * then a second notification will be given in Event Dispatch Thread once the image texture is loaded. 
 63  * If the texture is in cache, it will be notified immediately to the given <code>textureObserver</code>.
 64  * @param {URLContent} content  an object containing an image
 65  * @param {number}  [angle]       the rotation angle applied to the image
 66  * @param {boolean} [synchronous] if <code>true</code>, this method will return only once image content is loaded.
 67  * @param {{textureUpdated, textureError, progression}} textureObserver 
 68  *             the observer that will be notified once the texture is available. 
 69  *             It may define <code>textureUpdated(textureImage)</code>, <code>textureError(error)</code>, 
 70  *             <code>progression(part, info, percentage)</code> optional methods 
 71  *             with <code>textureImage<code> being an instance of <code>Image</code>, 
 72  *             <code>error</code>, <code>part</code>, <code>info</code> strings 
 73  *             and <code>percentage</code> a number.
 74  */
 75 TextureManager.prototype.loadTexture = function(content, angle, synchronous, textureObserver) {
 76   if (synchronous === undefined) {
 77     // 2 parameters (content, textureObserver)
 78     textureObserver = angle;
 79     angle = 0;
 80     synchronous = false;
 81   } else if (textureObserver === undefined) {
 82     // 3 parameters (content, synchronous, textureObserver)
 83     textureObserver = synchronous;
 84     synchronous = false;
 85   }
 86   if (synchronous 
 87       && !content.isStreamURLReady()) {
 88     throw new IllegalStateException("Can't run synchronously with unavailable URL");
 89   }    
 90   var textureManager = this;
 91   var contentUrl = content.getURL();
 92   content.getStreamURL({
 93       urlReady: function(streamUrl) {
 94         if (contentUrl in textureManager.loadedTextureImages) {
 95           if (textureObserver.textureUpdated !== undefined) {
 96             textureObserver.textureUpdated(textureManager.loadedTextureImages [contentUrl]);
 97           }
 98         } else if (synchronous) {
 99           textureManager.load(streamUrl, synchronous, {
100               textureLoaded : function(textureImage) {
101                 // Note that angle is managed with appearance#setTextureTransform
102                 textureManager.loadedTextureImages [contentUrl] = textureImage;
103                 if (textureObserver.textureUpdated !== undefined) {
104                   textureObserver.textureUpdated(textureImage);
105                 }
106               },
107               textureError : function(error) {
108                 if (textureObserver.textureError !== undefined) {
109                   textureObserver.textureError(error);
110                 }
111               },
112               progression : function(part, info, percentage) {
113                 if (textureObserver.progression !== undefined) {
114                   textureObserver.progression(part, info, percentage);
115                 }
116               }
117             });
118         } else {
119           if (contentUrl in textureManager.loadingTextureObservers) {
120             // If observers list exists, content texture is already being loaded
121             // register observer for future notification
122             textureManager.loadingTextureObservers [contentUrl].push(textureObserver);
123           } else {
124             // Create a list of observers that will be notified once texture model is loaded
125             var observers = [];
126             observers.push(textureObserver);
127             textureManager.loadingTextureObservers [contentUrl] = observers;
128             textureManager.load(streamUrl, synchronous, {
129                 textureLoaded : function(textureImage) {
130                   var observers = textureManager.loadingTextureObservers [contentUrl];
131                   if (observers) {
132                     delete textureManager.loadingTextureObservers [contentUrl];
133                     // Note that angle is managed with appearance#setTextureTransform
134                     textureManager.loadedTextureImages [contentUrl] = textureImage;
135                     for (var i = 0; i < observers.length; i++) {
136                       if (observers [i].textureUpdated !== undefined) {
137                         observers [i].textureUpdated(textureImage);
138                       }
139                     }
140                   }
141                 },
142                 textureError : function(error) {
143                   var observers = textureManager.loadingTextureObservers [contentUrl];
144                   if (observers) {
145                     delete textureManager.loadingTextureObservers [contentUrl];
146                     for (var i = 0; i < observers.length; i++) {
147                       if (observers [i].textureError !== undefined) {
148                         observers [i].textureError(error);
149                       }
150                     }
151                   }
152                 },
153                 progression : function(part, info, percentage) {
154                   var observers = textureManager.loadingTextureObservers [contentUrl];
155                   if (observers) {
156                     for (var i = 0; i < observers.length; i++) {
157                       if (observers [i].progression !== undefined) {
158                         observers [i].progression(part, info, percentage);
159                       }
160                     } 
161                   }
162                 }
163               });
164           }
165         }
166       },
167       urlError: function(status, error) {
168         textureObserver.textureError(status);
169       }    
170     });
171 }
172 
173 /**
174  * Manages loading of the image at the given <code>url</code>.
175  * @param {string}  url      the URL of the image
176  * @param {boolean} [synchronous] if <code>true</code>, this method will return only once image content is loaded.
177  * @param {{textureLoaded, textureError, progression}} loadingTextureObserver 
178  *             the observer that will be notified once the texture is available. 
179  *             It must define <code>textureLoaded(textureImage)</code>,  <code>textureError(error)</code>, 
180  *             <code>progression(part, info, percentage)</code> methods  
181  *             with <code>textureImage<code> being an instance of <code>Image</code>, 
182  *             <code>error</code>, <code>part</code>, <code>info</code> strings 
183  *             and <code>percentage</code> a number.
184  * @private
185  */
186 TextureManager.prototype.load = function(url, synchronous, loadingTextureObserver) {
187   loadingTextureObserver.progression(TextureManager.READING_TEXTURE, url, 0);
188   var textureImage = new Image();
189   textureImage.crossOrigin = "anonymous";
190   textureImage.url = url;
191   var imageErrorListener = function(ev) {
192       textureImage.removeEventListener("load", imageLoadingListener);
193       textureImage.removeEventListener("error", imageErrorListener);
194       loadingTextureObserver.textureError("Can't load " + url);
195     };
196   var imageLoadingListener = function() {
197       textureImage.removeEventListener("load", imageLoadingListener);
198       textureImage.removeEventListener("error", imageErrorListener);
199       loadingTextureObserver.progression(TextureManager.READING_TEXTURE, url, 1);
200       
201       if (textureImage.transparent === undefined) {
202         var request = new XMLHttpRequest();
203         request.open("GET", url, synchronous);
204         request.addEventListener("load", function() {
205             if (request.readyState === XMLHttpRequest.DONE 
206                 && (request.status === 0 || request.status === 200)) {
207               textureImage.transparent = ZIPTools.isTransparentImage(request.response);
208             }
209             loadingTextureObserver.textureLoaded(textureImage);
210           });
211         request.send();
212       } else {
213         loadingTextureObserver.textureLoaded(textureImage);
214       }
215     };
216   if (url.indexOf("jar:") === 0) {
217     var entrySeparatorIndex = url.indexOf("!/");
218     var imageEntryName = decodeURIComponent(url.substring(entrySeparatorIndex + 2));
219     var jarUrl = url.substring(4, entrySeparatorIndex);
220     ZIPTools.getZIP(jarUrl, synchronous,
221         {
222           zipReady : function(zip) {
223             try {
224               textureImage.addEventListener("load", imageLoadingListener);
225               textureImage.addEventListener("error", imageErrorListener);
226               var imageEntry = zip.file(imageEntryName);
227               var imageData = imageEntry.asBinary();
228               var base64Image = btoa(imageData);
229               // Detect quickly if the image is a PNG using transparency
230               textureImage.transparent = ZIPTools.isTransparentImage(imageData);
231               textureImage.src = "data:image;base64," + base64Image;
232               // If image is already here or if image loading must be synchronous 
233               if (textureImage.width !== 0
234                   || synchronous) {
235                 imageLoadingListener();
236               }
237             } catch (ex) {
238               this.zipError(ex);
239             }
240           },
241           zipError : function(error) {
242             loadingTextureObserver.textureError("Can't load " + jarUrl); 
243           },
244           progression : function(part, info, percentage) {
245             loadingTextureObserver.progression(part, info, percentage);
246           }
247         });
248   } else {
249     textureImage.addEventListener("load", imageLoadingListener);
250     textureImage.addEventListener("error", imageErrorListener);
251     // Prepare download
252     textureImage.src = url;
253     if (textureImage.width !== 0) {
254       // Image is already here
255       imageLoadingListener();
256     } 
257   }
258 }
259 
260 /**
261  * Returns <code>true</code> if the texture is shared and its image contains 
262  * at least one transparent pixel.
263  * @return {boolean}
264  */
265 TextureManager.prototype.isTextureTransparent = function(textureImage) {
266   return textureImage.transparent === true;
267 }
268 
269 /**
270  * Returns the width of the given texture once its rotation angle is applied.
271  * @return {number}
272  */
273 TextureManager.prototype.getRotatedTextureWidth = function(texture) {
274   var angle = texture.getAngle();
275   if (angle !== 0) {
276     return Math.round(Math.abs(texture.getWidth() * Math.cos(angle)) 
277         + Math.abs(texture.getHeight() * Math.sin(angle)));
278   } else {
279     return texture.getWidth();
280   }
281 }
282 
283 /**
284  * Returns the height of the given texture once its rotation angle is applied.
285  * @return {number}
286  */
287 TextureManager.prototype.getRotatedTextureHeight = function(texture) {
288   var angle = texture.getAngle();
289   if (angle !== 0) {
290     return Math.round(Math.abs(texture.getWidth() * Math.sin(angle)) 
291         + Math.abs(texture.getHeight() * Math.cos(angle)));
292   } else {
293     return texture.getHeight();
294   }
295 }
296 
297 /**
298  * Returns an image for error purpose.
299  * @package
300  * @ignore
301  */
302 TextureManager.prototype.getErrorImage = function() {
303   if (TextureManager.errorImage === undefined) {
304     TextureManager.errorImage = this.getColoredImage("#FF0000");
305   }
306   return TextureManager.errorImage;
307 }
308 
309 /**
310  * Returns an image for wait purpose.
311  * @package
312  * @ignore
313  */
314 TextureManager.prototype.getWaitImage = function() {
315   if (TextureManager.waitImage === undefined) {
316     TextureManager.waitImage = this.getColoredImage("#FFFFFF");
317   }
318   return TextureManager.waitImage;
319 }
320 
321 /**
322  * Returns an image filled with a color.
323  * @param {string} color 
324  * @private
325  */
326 TextureManager.prototype.getColoredImage = function(color) {
327   // Create on the fly an image of 2x2 pixels
328   var canvas = document.createElement('canvas');
329   canvas.width = 2;
330   canvas.height = 2;
331   var context = canvas.getContext('2d');
332   context.fillStyle = color;
333   context.fillRect(0, 0, 2, 2);
334   var coloredImageUrl = canvas.toDataURL();
335   var coloredImage = new Image();
336   coloredImage.url = coloredImageUrl;
337   coloredImage.src = coloredImageUrl;
338   return coloredImage;
339 }
340