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