├── image.png ├── blender_manifest.toml ├── licence.md ├── server └── server.py ├── setup_files.py ├── web ├── css │ └── style.css ├── js │ └── viewer.js └── index.html ├── README.md └── __init__.py /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berloop/blender-web-viewer/HEAD/image.png -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | id = "blendxweb" 3 | version = "1.0.0" 4 | name = "BlendXWeb" 5 | tagline = "Preview and export Blender scenes to interactive web viewers" 6 | maintainer = "Egret " 7 | type = "add-on" 8 | blender_version_min = "4.0.0" 9 | license = [ 10 | "SPDX:GPL-3.0-or-later", 11 | ] 12 | copyright = [ 13 | "2025 Egret Software", 14 | ] 15 | tags = ["Web", "Export", "Viewer", "3D View"] 16 | 17 | [permissions] 18 | files = "Export and serve 3D models and web content" -------------------------------------------------------------------------------- /licence.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | 4 | Copyright (C) 2025 Egret Software 5 | egretfx@gmail.com 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple HTTP server for serving Blender Web Preview files. 4 | Usage: python server.py [port] [directory] 5 | """ 6 | 7 | import sys 8 | import os 9 | from http.server import HTTPServer, SimpleHTTPRequestHandler 10 | import socketserver 11 | 12 | def run_server(port, directory): 13 | """Run a simple HTTP server on the specified port and directory""" 14 | 15 | # Change to the specified directory 16 | os.chdir(directory) 17 | 18 | # Create a request handler that logs minimally 19 | class QuietHandler(SimpleHTTPRequestHandler): 20 | def log_message(self, format, *args): 21 | # Minimal logging to keep console clean 22 | print(f"HTTP: {format % args}") 23 | 24 | def end_headers(self): 25 | # Add CORS headers to allow loading from any origin 26 | self.send_header('Access-Control-Allow-Origin', '*') 27 | self.send_header('Access-Control-Allow-Methods', 'GET') 28 | self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate') 29 | super().end_headers() 30 | 31 | # Create a threaded HTTP server 32 | class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer): 33 | """Handle requests in a separate thread.""" 34 | daemon_threads = True 35 | 36 | # Create and start the server 37 | server = ThreadedHTTPServer(("", port), QuietHandler) 38 | print(f"Server started at http://localhost:{port}") 39 | print(f"Serving files from: {directory}") 40 | print("Press Ctrl+C to stop") 41 | 42 | try: 43 | server.serve_forever() 44 | except KeyboardInterrupt: 45 | print("Server stopped.") 46 | finally: 47 | server.server_close() 48 | 49 | if __name__ == "__main__": 50 | # Get command line arguments 51 | if len(sys.argv) < 3: 52 | print("Usage: python server.py [port] [directory]") 53 | sys.exit(1) 54 | 55 | port = int(sys.argv[1]) 56 | directory = sys.argv[2] 57 | 58 | # Run the server 59 | run_server(port, directory) -------------------------------------------------------------------------------- /setup_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | File structure setup script for Blender Web Preview addon 4 | Creates all the necessary directories and files 5 | """ 6 | 7 | import os 8 | import shutil 9 | import sys 10 | 11 | # Define the base directory (where this script is located) 12 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | # Define the directory structure 15 | DIRECTORIES = [ 16 | "server", 17 | "web", 18 | "web/css", 19 | "web/js", 20 | "web/assets" 21 | ] 22 | 23 | # List of files to create and their contents 24 | FILES = { 25 | "server/__init__.py": "# Server initialization file\n", 26 | "web/__init__.py": "# Web directory initialization file\n", 27 | "web/index.html": "# Will be replaced by actual HTML file\n", 28 | "web/css/style.css": "# Will be replaced by actual CSS file\n", 29 | "web/js/viewer.js": "# Will be replaced by actual JS file\n", 30 | "web/assets/__init__.py": "# Assets directory initialization file\n" 31 | } 32 | 33 | def create_directories(): 34 | """Create the directory structure""" 35 | for directory in DIRECTORIES: 36 | dir_path = os.path.join(BASE_DIR, directory) 37 | if not os.path.exists(dir_path): 38 | print(f"Creating directory: {directory}") 39 | os.makedirs(dir_path) 40 | else: 41 | print(f"Directory already exists: {directory}") 42 | 43 | def create_files(): 44 | """Create necessary files""" 45 | for file_path, content in FILES.items(): 46 | full_path = os.path.join(BASE_DIR, file_path) 47 | if not os.path.exists(full_path): 48 | print(f"Creating file: {file_path}") 49 | with open(full_path, 'w') as f: 50 | f.write(content) 51 | else: 52 | print(f"File already exists: {file_path}") 53 | 54 | def copy_asset_files(): 55 | """Copy asset files from script directory to addon directories""" 56 | # You can add additional file copying logic here if needed 57 | pass 58 | 59 | def main(): 60 | """Main function""" 61 | print("Setting up file structure for Blender Web Preview addon...") 62 | 63 | create_directories() 64 | create_files() 65 | copy_asset_files() 66 | 67 | print("File structure setup complete!") 68 | 69 | if __name__ == "__main__": 70 | main() -------------------------------------------------------------------------------- /web/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Blender Web Preview CSS 3 | * Styles for the web viewer 4 | */ 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | font-family: 'Arial', sans-serif; 14 | background-color: #1a1a1a; 15 | color: #e0e0e0; 16 | overflow: hidden; 17 | } 18 | 19 | #container { 20 | display: flex; 21 | width: 100vw; 22 | height: 100vh; 23 | } 24 | 25 | #viewer { 26 | flex: 1; 27 | position: relative; 28 | } 29 | 30 | #viewer canvas { 31 | width: 100%; 32 | height: 100%; 33 | display: block; 34 | } 35 | 36 | #controls { 37 | width: 300px; 38 | background-color: #2a2a2a; 39 | border-left: 1px solid #3a3a3a; 40 | padding: 15px; 41 | overflow-y: auto; 42 | } 43 | 44 | #info-panel { 45 | margin-bottom: 20px; 46 | padding-bottom: 15px; 47 | border-bottom: 1px solid #3a3a3a; 48 | } 49 | 50 | #scene-title { 51 | font-size: 18px; 52 | margin-bottom: 10px; 53 | } 54 | 55 | #scene-stats { 56 | font-size: 14px; 57 | color: #aaaaaa; 58 | } 59 | 60 | .control-group { 61 | margin-bottom: 20px; 62 | padding-bottom: 15px; 63 | border-bottom: 1px solid #3a3a3a; 64 | } 65 | 66 | .control-group h3 { 67 | font-size: 16px; 68 | margin-bottom: 10px; 69 | color: #cccccc; 70 | } 71 | 72 | .control-row { 73 | display: flex; 74 | margin-bottom: 10px; 75 | align-items: center; 76 | } 77 | 78 | button { 79 | background-color: #3e3e3e; 80 | color: #e0e0e0; 81 | border: none; 82 | padding: 8px 12px; 83 | border-radius: 4px; 84 | margin-right: 5px; 85 | cursor: pointer; 86 | transition: background-color 0.2s; 87 | } 88 | 89 | button:hover { 90 | background-color: #4e4e4e; 91 | } 92 | 93 | button:active { 94 | background-color: #5e5e5e; 95 | } 96 | 97 | input[type="checkbox"] { 98 | margin-right: 8px; 99 | } 100 | 101 | input[type="range"] { 102 | width: 100%; 103 | background-color: #3e3e3e; 104 | height: 5px; 105 | border-radius: 2px; 106 | outline: none; 107 | -webkit-appearance: none; 108 | } 109 | 110 | input[type="range"]::-webkit-slider-thumb { 111 | -webkit-appearance: none; 112 | width: 15px; 113 | height: 15px; 114 | background-color: #e0e0e0; 115 | border-radius: 50%; 116 | cursor: pointer; 117 | } 118 | 119 | label { 120 | display: flex; 121 | align-items: center; 122 | cursor: pointer; 123 | } 124 | 125 | #animation-time { 126 | font-size: 14px; 127 | color: #aaaaaa; 128 | } 129 | 130 | /* Loading indicator */ 131 | .loading-overlay { 132 | position: absolute; 133 | top: 0; 134 | left: 0; 135 | width: 100%; 136 | height: 100%; 137 | background-color: rgba(0, 0, 0, 0.7); 138 | display: flex; 139 | justify-content: center; 140 | align-items: center; 141 | z-index: 1000; 142 | } 143 | 144 | .loading-spinner { 145 | width: 50px; 146 | height: 50px; 147 | border: 5px solid rgba(255, 255, 255, 0.3); 148 | border-radius: 50%; 149 | border-top-color: #fff; 150 | animation: spin 1s ease-in-out infinite; 151 | } 152 | 153 | @keyframes spin { 154 | to { 155 | transform: rotate(360deg); 156 | } 157 | } 158 | 159 | /* Responsive design */ 160 | @media (max-width: 768px) { 161 | #container { 162 | flex-direction: column; 163 | } 164 | 165 | #controls { 166 | width: 100%; 167 | height: 300px; 168 | border-left: none; 169 | border-top: 1px solid #3a3a3a; 170 | } 171 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Introduction 5 | 6 | **BlendXWeb** is free and open-source Blender addon that allows you to effortlessly preview your Blender scenes in a web browser and export them as standalone web packages. With just a few clicks, you can convert your 3D scenes into interactive, web-accessible viewers, making it easier to share and showcase your work online. 7 | 8 | ![BlendXWeb Preview](./image.png) 9 | 10 | 11 | 12 | ## Installation and Usage Guide 13 | 14 | This addon allows you to preview your Blender scenes in a web browser and export them as standalone web packages. 15 | 16 | ## Prerequisites 17 | 18 | - Blender 4.0 or newer 19 | - Web browser with WebGL support (Chrome, Firefox, Edge, Safari) 20 | - Python 3.7+ (typically included with Blender) 21 | 22 | ## Installation 23 | 24 | ### Method 1: Via Blender's Addon Installer 25 | 26 | 1. Download the addon as a ZIP file (do not extract it) 27 | 2. Open Blender and go to Edit > Preferences 28 | 3. Select the "Add-ons" tab 29 | 4. Click "Install..." button at the top right 30 | 5. Navigate to and select the downloaded ZIP file 31 | 6. Click "Install Add-on" 32 | 7. Enable the addon by checking the box next to "3D View: BlendXWeb" 33 | 34 | ### Method 2: Manual Installation 35 | 36 | 1. Download and extract the addon files 37 | 2. Locate your Blender addons directory: 38 | - Windows: `%APPDATA%\Blender Foundation\Blender\4.0\scripts\addons` 39 | - macOS: `~/Library/Application Support/Blender/4.0/scripts/addons` 40 | - Linux: `~/.config/blender/4.0/scripts/addons` 41 | 3. Copy the entire `blender_web_preview` folder to the addons directory 42 | 4. Start Blender and go to Edit > Preferences > Add-ons 43 | 5. Search for "BlendXWeb" and enable the addon 44 | 45 | ## File Structure Setup 46 | 47 | After installation, you need to run the file structure setup script once to create the necessary directories: 48 | 49 | 1. Open a terminal/command prompt 50 | 2. Navigate to the addon directory 51 | 3. Run `python setup_files.py` 52 | 53 | ## Using the Addon 54 | 55 | ### Accessing the Addon 56 | 57 | Once installed, you can access the addon from the 3D View sidebar: 58 | 59 | 1. Open the sidebar by pressing `N` in the 3D View 60 | 2. Select the "Web Preview/BlendXWeb" tab 61 | 62 | ### Previewing in Browser 63 | 64 | 1. Set up your scene in Blender 65 | 2. Click the "Preview in Browser" button in the addon panel 66 | 3. A local server will start and your default web browser will open showing your scene 67 | 4. Use the controls in the browser to navigate and interact with the scene 68 | 69 | ### Exporting for Web 70 | 71 | 1. Set up your scene in Blender 72 | 2. Click the "Export Scene to Web" button in the addon panel 73 | 3. Choose a destination folder and filename 74 | 4. Click "Export" 75 | 5. A ZIP file will be created containing all necessary files for web viewing 76 | 77 | ## Web Viewer Controls 78 | 79 | The web viewer provides several controls to interact with your scene: 80 | 81 | ### Camera Controls 82 | - Left Mouse: Rotate the view 83 | - Middle Mouse / Scroll: Zoom in/out 84 | - Right Mouse: Pan the view 85 | - Top/Front/Side buttons: Jump to standard views 86 | - Reset Camera: Return to the initial view 87 | 88 | ### Display Options 89 | - Wireframe: Toggle wireframe mode 90 | - Grid: Toggle the reference grid 91 | - Lights: Toggle scene lights (excluding ambient light) 92 | 93 | ### Animation Controls 94 | - Play: Start the animation 95 | - Pause: Pause the animation 96 | - Stop: Stop and reset the animation 97 | - Slider: Scrub through the animation timeline 98 | 99 | ## Troubleshooting 100 | 101 | ### Server Issues 102 | - If the preview doesn't open, check if port 3000 is available or in use 103 | - Try stopping and restarting the server from the addon panel 104 | 105 | ### Export Issues 106 | - Make sure your Blender scene has been saved 107 | - Check if you have write permissions for the export directory 108 | - Ensure all textures are properly packed or referenced 109 | 110 | ### Web Viewer Issues 111 | - Make sure your browser supports WebGL 112 | - Check the browser console for any error messages 113 | - Try using a different browser if issues persist 114 | 115 | ## License 116 | 117 | This addon is licensed under the MIT License. See LICENSE file for details. 118 | 119 | ## Support 120 | 121 | For issues, feature requests, or contributions, please visit the project repository or contact me directly at egretfx@gmail.com. Don't spam me or i will make you drink cocoa tea. 122 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # BlenderWebPreview - Blender Addon for Web Preview and Export 2 | # For Blender 4.0+ 3 | 4 | bl_info = { 5 | "name": "BlendXWeb", 6 | "author": "Egret", 7 | "version": (1, 0), 8 | "blender": (4, 0, 0), 9 | "location": "View3D > Sidebar > BlendXweb", 10 | "description": "Preview and export Blender scenes to web browsers", 11 | "warning": "", 12 | "doc_url": "", 13 | "category": "3D View", 14 | } 15 | 16 | import bpy 17 | import os 18 | import sys 19 | import json 20 | import threading 21 | import webbrowser 22 | import shutil 23 | import socket 24 | import tempfile 25 | from pathlib import Path 26 | import subprocess 27 | 28 | # Global server variable 29 | preview_server = None 30 | 31 | # ------------------------------------ 32 | # Server Component 33 | # ------------------------------------ 34 | 35 | class WebPreviewServer: 36 | """Simple HTTP server to serve the Blender scene preview""" 37 | 38 | def __init__(self): 39 | self.server_process = None 40 | self.port = 3000 # Fixed port to 3000 41 | self.temp_dir = None 42 | self.is_running = False 43 | 44 | def find_available_port(self): 45 | """Find an available port for the server""" 46 | # Using fixed port 3000 47 | self.port = 3000 48 | return self.port 49 | 50 | def start_server(self): 51 | """Start the HTTP server""" 52 | if self.is_running: 53 | return 54 | 55 | # Create a temporary directory for serving files 56 | self.temp_dir = tempfile.mkdtemp() 57 | 58 | # Find an available port 59 | self.find_available_port() 60 | 61 | # Set up the server using Python's built-in HTTP server 62 | server_script = os.path.join(os.path.dirname(__file__), "server", "server.py") 63 | 64 | # Start the server process 65 | try: 66 | self.server_process = subprocess.Popen( 67 | [sys.executable, server_script, str(self.port), self.temp_dir], 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE 70 | ) 71 | self.is_running = True 72 | print(f"Server started on port {self.port}") 73 | except Exception as e: 74 | print(f"Failed to start server: {e}") 75 | self.is_running = False 76 | 77 | def stop_server(self): 78 | """Stop the HTTP server""" 79 | if not self.is_running: 80 | return 81 | 82 | # Terminate the server process 83 | if self.server_process: 84 | self.server_process.terminate() 85 | self.server_process = None 86 | 87 | # Clean up the temporary directory - with error handling 88 | if self.temp_dir and os.path.exists(self.temp_dir): 89 | try: 90 | shutil.rmtree(self.temp_dir) 91 | except Exception as e: 92 | print(f"Warning: Could not remove temp directory: {e}") 93 | # We'll just let it be and Windows will clean it up later 94 | self.temp_dir = None 95 | 96 | self.is_running = False 97 | print("Server stopped") 98 | 99 | def get_url(self): 100 | """Get the URL for the web preview""" 101 | if not self.is_running: 102 | return None 103 | return f"http://localhost:{self.port}" 104 | 105 | # ------------------------------------ 106 | # Export Functions 107 | # ------------------------------------ 108 | 109 | def export_scene_to_gltf(context, filepath, export_settings): 110 | """Export the current scene to glTF format""" 111 | 112 | # Configure export settings for glTF - using GLB format instead 113 | try: 114 | bpy.ops.export_scene.gltf( 115 | filepath=filepath, 116 | export_format='GLB', # Changed from GLTF_EMBEDDED to GLB 117 | export_selected=export_settings.get('export_selected', False), 118 | export_animations=export_settings.get('export_animations', True), 119 | export_cameras=export_settings.get('export_cameras', True), 120 | export_lights=export_settings.get('export_lights', True) 121 | ) 122 | except TypeError as e: 123 | # If the above parameters don't work, try with minimal parameters 124 | print(f"Trying minimal export parameters due to error: {e}") 125 | bpy.ops.export_scene.gltf( 126 | filepath=filepath, 127 | export_format='GLB' # Only use the format parameter 128 | ) 129 | except Exception as e: 130 | print(f"Error during export: {e}") 131 | raise 132 | 133 | return filepath 134 | 135 | def generate_preview_files(context, temp_dir): 136 | """Generate all necessary files for web preview""" 137 | # Export the scene to glTF 138 | gltf_path = os.path.join(temp_dir, "scene.glb") # Using .glb format 139 | export_settings = { 140 | 'export_selected': False, # Export entire scene 141 | 'export_animations': True, 142 | 'export_cameras': True, 143 | 'export_lights': True 144 | } 145 | export_scene_to_gltf(context, gltf_path, export_settings) 146 | 147 | # Copy the web viewer files 148 | viewer_files_dir = os.path.join(os.path.dirname(__file__), "web") 149 | 150 | # Ensure these key files exist and are not empty 151 | key_files = ['index.html', 'js/viewer.js', 'css/style.css'] 152 | for file in key_files: 153 | source_file = os.path.join(viewer_files_dir, file) 154 | if not os.path.exists(source_file) or os.path.getsize(source_file) == 0: 155 | print(f"Warning: Required file {file} is missing or empty!") 156 | 157 | # First copy individual files to ensure they're properly handled 158 | index_source = os.path.join(viewer_files_dir, "index.html") 159 | index_dest = os.path.join(temp_dir, "index.html") 160 | 161 | if os.path.exists(index_source) and os.path.getsize(index_source) > 0: 162 | shutil.copy2(index_source, index_dest) 163 | print(f"Copied index.html: {os.path.getsize(index_dest)} bytes") 164 | else: 165 | print(f"Error: index.html is missing or empty! Path: {index_source}") 166 | # Create a basic index.html if missing 167 | with open(index_dest, 'w') as f: 168 | f.write(""" 169 | 170 | 171 | 172 | 173 | 174 | BlendXweb | By Egret 175 | 176 | 177 | 178 |
179 |
180 |
181 |
182 |

Blender Scene

183 |
184 |
185 |
186 |
187 |

Camera

188 | 189 |
190 | 191 | 192 | 193 |
194 |
195 |
196 |

Display

197 |
198 | 202 |
203 |
204 | 208 |
209 |
210 | 214 |
215 |
216 |
217 |

Animation

218 |
219 | 220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 | 0 / 0 228 |
229 |
230 |
231 |
232 |
233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | """) 244 | print("Created default index.html") 245 | 246 | # Make sure directories exist before copying files 247 | js_dir = os.path.join(temp_dir, "js") 248 | css_dir = os.path.join(temp_dir, "css") 249 | 250 | os.makedirs(js_dir, exist_ok=True) 251 | os.makedirs(css_dir, exist_ok=True) 252 | 253 | # Copy JS file 254 | js_source = os.path.join(viewer_files_dir, "js", "viewer.js") 255 | js_dest = os.path.join(js_dir, "viewer.js") 256 | 257 | if os.path.exists(js_source) and os.path.getsize(js_source) > 0: 258 | shutil.copy2(js_source, js_dest) 259 | print(f"Copied viewer.js: {os.path.getsize(js_dest)} bytes") 260 | else: 261 | print(f"Error: viewer.js is missing or empty! Path: {js_source}") 262 | 263 | # Copy CSS file 264 | css_source = os.path.join(viewer_files_dir, "css", "style.css") 265 | css_dest = os.path.join(css_dir, "style.css") 266 | 267 | if os.path.exists(css_source) and os.path.getsize(css_source) > 0: 268 | shutil.copy2(css_source, css_dest) 269 | print(f"Copied style.css: {os.path.getsize(css_dest)} bytes") 270 | else: 271 | print(f"Error: style.css is missing or empty! Path: {css_source}") 272 | 273 | # Create a scene info JSON file with metadata 274 | scene_info = { 275 | "title": bpy.path.basename(bpy.data.filepath) or "Untitled Scene", 276 | "objects": len(bpy.data.objects), 277 | "has_animations": any(obj.animation_data for obj in bpy.data.objects) 278 | } 279 | 280 | with open(os.path.join(temp_dir, "scene_info.json"), 'w') as f: 281 | json.dump(scene_info, f) 282 | 283 | return temp_dir 284 | 285 | def package_for_export(context, export_path): 286 | """Package all files into a standalone web export""" 287 | # Create a temporary directory for staging files 288 | try: 289 | temp_dir = tempfile.mkdtemp() 290 | 291 | # Generate all the preview files 292 | generate_preview_files(context, temp_dir) 293 | 294 | # Create a zip file with all contents 295 | shutil.make_archive(export_path, 'zip', temp_dir) 296 | 297 | # Clean up with error handling 298 | try: 299 | shutil.rmtree(temp_dir) 300 | except: 301 | print("Warning: Could not remove temporary directory after export") 302 | 303 | return export_path + ".zip" 304 | except Exception as e: 305 | print(f"Error during export: {e}") 306 | return None 307 | 308 | # ------------------------------------ 309 | # Operators 310 | # ------------------------------------ 311 | 312 | class WEB_PREVIEW_OT_preview_scene(bpy.types.Operator): 313 | """Preview the current scene in a web browser""" 314 | bl_idname = "web_preview.preview_scene" 315 | bl_label = "Preview in Browser" 316 | 317 | def execute(self, context): 318 | # Get the server instance 319 | global preview_server 320 | 321 | # Initialize server if needed 322 | if preview_server is None: 323 | preview_server = WebPreviewServer() 324 | 325 | # Start the server if it's not already running 326 | if not preview_server.is_running: 327 | preview_server.start_server() 328 | 329 | # Generate preview files 330 | try: 331 | generate_preview_files(context, preview_server.temp_dir) 332 | except Exception as e: 333 | self.report({'ERROR'}, f"Error generating preview: {str(e)}") 334 | return {'CANCELLED'} 335 | 336 | # Open the web browser 337 | webbrowser.open(preview_server.get_url()) 338 | 339 | return {'FINISHED'} 340 | 341 | class WEB_PREVIEW_OT_export_scene(bpy.types.Operator): 342 | """Export the current scene as a standalone web package""" 343 | bl_idname = "web_preview.export_scene" 344 | bl_label = "Export Scene to Web" 345 | 346 | filepath: bpy.props.StringProperty( 347 | name="Export Path", 348 | description="Path to export the web package", 349 | default="", 350 | subtype='FILE_PATH' 351 | ) 352 | 353 | def invoke(self, context, event): 354 | # Set default filename based on blend file 355 | blend_filepath = bpy.data.filepath 356 | if blend_filepath: 357 | filename = os.path.splitext(os.path.basename(blend_filepath))[0] + "_web" 358 | self.filepath = os.path.join(os.path.dirname(blend_filepath), filename) 359 | else: 360 | self.filepath = "untitled_web" 361 | 362 | context.window_manager.fileselect_add(self) 363 | return {'RUNNING_MODAL'} 364 | 365 | def execute(self, context): 366 | # Package and export the scene 367 | try: 368 | export_path = package_for_export(context, self.filepath) 369 | if export_path: 370 | self.report({'INFO'}, f"Scene exported to {export_path}") 371 | return {'FINISHED'} 372 | else: 373 | self.report({'ERROR'}, "Export failed") 374 | return {'CANCELLED'} 375 | except Exception as e: 376 | self.report({'ERROR'}, f"Export error: {str(e)}") 377 | return {'CANCELLED'} 378 | 379 | class WEB_PREVIEW_OT_stop_server(bpy.types.Operator): 380 | """Stop the web preview server""" 381 | bl_idname = "web_preview.stop_server" 382 | bl_label = "Stop Server" 383 | 384 | def execute(self, context): 385 | global preview_server 386 | 387 | if preview_server and preview_server.is_running: 388 | preview_server.stop_server() 389 | self.report({'INFO'}, "Web preview server stopped") 390 | else: 391 | self.report({'INFO'}, "Server is not running") 392 | 393 | return {'FINISHED'} 394 | 395 | # ------------------------------------ 396 | # UI 397 | # ------------------------------------ 398 | 399 | class WEB_PREVIEW_PT_panel(bpy.types.Panel): 400 | """Panel for web preview controls""" 401 | bl_label = "BlendXweb By Egret" 402 | bl_idname = "WEB_PREVIEW_PT_panel" 403 | bl_space_type = 'VIEW_3D' 404 | bl_region_type = 'UI' 405 | bl_category = 'BlendXweb' 406 | 407 | def draw(self, context): 408 | layout = self.layout 409 | 410 | # Get server status 411 | global preview_server 412 | 413 | # Preview section 414 | box = layout.box() 415 | box.label(text="Preview") 416 | 417 | row = box.row() 418 | row.operator("web_preview.preview_scene", icon='WORLD') 419 | 420 | # Server status 421 | is_running = preview_server and preview_server.is_running 422 | box.label(text=f"Server: {'Running' if is_running else 'Offline'}") 423 | 424 | if is_running: 425 | box.operator("web_preview.stop_server", icon='X') 426 | box.label(text=f"Port: {preview_server.port}") 427 | 428 | # Export section 429 | box = layout.box() 430 | box.label(text="Export") 431 | box.operator("web_preview.export_scene", icon='EXPORT') 432 | 433 | # ------------------------------------ 434 | # Addon Preferences 435 | # ------------------------------------ 436 | 437 | class WebPreviewPreferences(bpy.types.AddonPreferences): 438 | bl_idname = __name__ 439 | 440 | def draw(self, context): 441 | layout = self.layout 442 | layout.label(text="Blender Web Preview/BlendXweb Settings") 443 | # TODO: Add global addon settings here 444 | 445 | # ------------------------------------ 446 | # Registration 447 | # ------------------------------------ 448 | 449 | classes = ( 450 | WebPreviewPreferences, 451 | WEB_PREVIEW_OT_preview_scene, 452 | WEB_PREVIEW_OT_export_scene, 453 | WEB_PREVIEW_OT_stop_server, 454 | WEB_PREVIEW_PT_panel 455 | ) 456 | 457 | def register(): 458 | for cls in classes: 459 | bpy.utils.register_class(cls) 460 | 461 | def unregister(): 462 | # Stop the server if it's running 463 | global preview_server 464 | if preview_server and preview_server.is_running: 465 | preview_server.stop_server() 466 | 467 | for cls in reversed(classes): 468 | bpy.utils.unregister_class(cls) 469 | 470 | if __name__ == "__main__": 471 | register() -------------------------------------------------------------------------------- /web/js/viewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Blender Web Preview Viewer 3 | * Main JavaScript file for rendering and controlling the 3D scene 4 | */ 5 | 6 | // At the top of your viewer.js file, add this check 7 | if (!window.OrbitControls && THREE.OrbitControls) { 8 | window.OrbitControls = THREE.OrbitControls; 9 | } 10 | 11 | if (!window.GLTFLoader && THREE.GLTFLoader) { 12 | window.GLTFLoader = THREE.GLTFLoader; 13 | } 14 | 15 | if (!window.DRACOLoader && THREE.DRACOLoader) { 16 | window.DRACOLoader = THREE.DRACOLoader; 17 | } 18 | 19 | window.addEventListener('DOMContentLoaded', function() { 20 | if (window.location.protocol === 'file:') { 21 | const warningDiv = document.createElement('div'); 22 | warningDiv.style.position = 'fixed'; 23 | warningDiv.style.top = '0'; 24 | warningDiv.style.left = '0'; 25 | warningDiv.style.width = '100%'; 26 | warningDiv.style.backgroundColor = 'rgba(255, 50, 50, 0.9)'; 27 | warningDiv.style.color = 'white'; 28 | warningDiv.style.padding = '10px'; 29 | warningDiv.style.zIndex = '9999'; 30 | warningDiv.style.textAlign = 'center'; 31 | warningDiv.innerHTML = 'This 3D viewer may not work when opened directly from your computer. ' + 32 | 'For best results:
1) Use a web server (like VS Code Live Server) ' + 33 | 'or
2) Upload these files to a web hosting service.'; 34 | document.body.appendChild(warningDiv); 35 | } 36 | }); 37 | 38 | // Global variables 39 | let scene, camera, renderer, controls; 40 | let mixer, clock, animationActions = []; 41 | let grid, sceneInfo; 42 | let isPlaying = false; 43 | let animationDuration = 0; 44 | 45 | // DOM elements 46 | const viewer = document.getElementById("viewer"); 47 | const sceneTitle = document.getElementById("scene-title"); 48 | const sceneStats = document.getElementById("scene-stats"); 49 | const animationSlider = document.getElementById("animation-slider"); 50 | const animationTime = document.getElementById("animation-time"); 51 | const animationControls = document.getElementById("animation-controls"); 52 | 53 | // Initialize the viewer 54 | function init() { 55 | // Create loading overlay 56 | const loadingOverlay = document.createElement("div"); 57 | loadingOverlay.className = "loading-overlay"; 58 | loadingOverlay.innerHTML = '
'; 59 | viewer.appendChild(loadingOverlay); 60 | 61 | // Initialize Three.js scene immediately without waiting for scene_info.json 62 | initScene(); 63 | 64 | // Try to load scene info, but continue even if it fails 65 | fetch("scene_info.json") 66 | .then((response) => response.json()) 67 | .then((data) => { 68 | console.log("Scene info loaded:", data); 69 | sceneInfo = data; 70 | sceneTitle.textContent = data.title; 71 | sceneStats.textContent = `Objects: ${data.objects}`; 72 | 73 | // Hide animation controls if there are no animations 74 | if (!data.has_animations) { 75 | animationControls.style.display = "none"; 76 | } 77 | }) 78 | .catch((error) => { 79 | console.log("Error loading scene info:", error); 80 | // Continue anyway 81 | sceneTitle.textContent = "Blender Scene"; 82 | sceneStats.textContent = "No scene info available"; 83 | }) 84 | .finally(() => { 85 | // Load the model after scene is initialized, regardless of scene_info status 86 | loadModel(); 87 | }); 88 | } 89 | 90 | // Initialize Three.js scene with BETTER LIGHTING 91 | function initScene() { 92 | console.log("Initializing Three.js scene"); 93 | 94 | // Create scene 95 | scene = new THREE.Scene(); 96 | scene.background = new THREE.Color(0x1a1a1a); 97 | 98 | // Create camera 99 | camera = new THREE.PerspectiveCamera( 100 | 45, 101 | viewer.clientWidth / viewer.clientHeight, 102 | 0.1, 103 | 1000 104 | ); 105 | camera.position.set(5, 5, 5); 106 | 107 | // Create renderer 108 | renderer = new THREE.WebGLRenderer({ antialias: true }); 109 | renderer.setSize(viewer.clientWidth, viewer.clientHeight); 110 | renderer.setPixelRatio(window.devicePixelRatio); 111 | renderer.shadowMap.enabled = true; 112 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; 113 | viewer.appendChild(renderer.domElement); 114 | 115 | try { 116 | controls = new OrbitControls(camera, renderer.domElement); 117 | controls.enableDamping = true; 118 | controls.dampingFactor = 0.05; 119 | } catch (e) { 120 | console.error("Error creating orbit controls:", e); 121 | console.log("Attempting alternative orbit controls initialization"); 122 | 123 | if (typeof OrbitControls !== "undefined") { 124 | controls = new OrbitControls(camera, renderer.domElement); 125 | controls.enableDamping = true; 126 | controls.dampingFactor = 0.05; 127 | } else { 128 | console.error("OrbitControls not available. Scene will be static."); 129 | } 130 | } 131 | 132 | // Add grid 133 | grid = new THREE.GridHelper(10, 10, 0x888888, 0x444444); 134 | scene.add(grid); 135 | 136 | // Make scene and renderer globally accessible for inspector 137 | window.scene = scene; 138 | window.renderer = renderer; 139 | 140 | // ENHANCED LIGHTING SETUP - LIT FROM ALL ANGLES 141 | 142 | // Brighter ambient light for overall illumination 143 | const ambientLight = new THREE.AmbientLight(0xffffff, 1.2); 144 | scene.add(ambientLight); 145 | 146 | // Main directional light (key light) - brighter 147 | const keyLight = new THREE.DirectionalLight(0xffffff, 1.5); 148 | keyLight.position.set(5, 10, 7.5); 149 | keyLight.castShadow = true; 150 | keyLight.shadow.mapSize.width = 2048; 151 | keyLight.shadow.mapSize.height = 2048; 152 | keyLight.shadow.camera.near = 0.1; 153 | keyLight.shadow.camera.far = 50; 154 | keyLight.shadow.camera.left = -10; 155 | keyLight.shadow.camera.right = 10; 156 | keyLight.shadow.camera.top = 10; 157 | keyLight.shadow.camera.bottom = -10; 158 | scene.add(keyLight); 159 | 160 | // Fill light from the left 161 | const fillLight = new THREE.DirectionalLight(0xffffff, 0.8); 162 | fillLight.position.set(-5, 5, 5); 163 | scene.add(fillLight); 164 | 165 | // Back light for rim lighting 166 | const backLight = new THREE.DirectionalLight(0xffffff, 0.6); 167 | backLight.position.set(0, 5, -5); 168 | scene.add(backLight); 169 | 170 | // Additional side lights for full coverage 171 | const leftLight = new THREE.DirectionalLight(0xffffff, 0.4); 172 | leftLight.position.set(-10, 3, 0); 173 | scene.add(leftLight); 174 | 175 | const rightLight = new THREE.DirectionalLight(0xffffff, 0.4); 176 | rightLight.position.set(10, 3, 0); 177 | scene.add(rightLight); 178 | 179 | // Store original light intensities for lighting control slider 180 | scene.traverse((child) => { 181 | if (child.isLight && child instanceof THREE.DirectionalLight) { 182 | child.userData.originalIntensity = child.intensity; 183 | } 184 | }); 185 | 186 | // Store original ambient light intensity too 187 | ambientLight.userData.originalIntensity = ambientLight.intensity; 188 | 189 | // Clock for animations 190 | clock = new THREE.Clock(); 191 | 192 | // Handle window resize 193 | window.addEventListener("resize", onWindowResize); 194 | 195 | // Start animation loop 196 | animate(); 197 | 198 | console.log("Scene initialized with enhanced lighting"); 199 | } 200 | 201 | // Load the GLTF model 202 | function loadModel() { 203 | console.log("Attempting to load model from: scene.glb"); 204 | 205 | const loader = new GLTFLoader(); 206 | 207 | try { 208 | const dracoLoader = new DRACOLoader(); 209 | dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/"); 210 | loader.setDRACOLoader(dracoLoader); 211 | } catch (e) { 212 | console.warn("Draco compression support not available:", e); 213 | } 214 | 215 | fetch("scene.glb") 216 | .then((response) => { 217 | if (!response.ok) { 218 | throw new Error(`HTTP error: ${response.status}`); 219 | } 220 | console.log( 221 | "scene.glb exists, file size:", 222 | response.headers.get("content-length"), 223 | "bytes" 224 | ); 225 | 226 | // Update file size in model info 227 | const fileSize = response.headers.get("content-length"); 228 | if (fileSize && window.setModelFileSize) { 229 | window.setModelFileSize(parseInt(fileSize)); 230 | } 231 | 232 | return true; 233 | }) 234 | .then(() => { 235 | loader.load( 236 | "scene.glb", 237 | function (gltf) { 238 | console.log("Model loaded successfully:", gltf); 239 | 240 | console.log("Scene contains:"); 241 | gltf.scene.traverse(function (child) { 242 | if (child.isMesh) { 243 | console.log("- Mesh:", child.name); 244 | // Enable shadows for better lighting 245 | child.castShadow = true; 246 | child.receiveShadow = true; 247 | } 248 | if (child.isLight) { 249 | console.log("- Light:", child.name); 250 | } 251 | if (child.isCamera) { 252 | console.log("- Camera:", child.name); 253 | } 254 | }); 255 | 256 | scene.add(gltf.scene); 257 | centerCameraOnModel(gltf.scene); 258 | 259 | // Update model information widget 260 | if (window.updateModelInfo) { 261 | window.updateModelInfo(gltf); 262 | } 263 | 264 | if (gltf.animations && gltf.animations.length > 0) { 265 | console.log("Model has animations:", gltf.animations.length); 266 | mixer = new THREE.AnimationMixer(gltf.scene); 267 | 268 | gltf.animations.forEach((clip) => { 269 | const action = mixer.clipAction(clip); 270 | animationActions.push(action); 271 | }); 272 | 273 | animationDuration = gltf.animations[0].duration; 274 | animationSlider.max = animationDuration; 275 | updateAnimationTime(0); 276 | 277 | if (animationActions.length > 0) { 278 | animationActions[0].play(); 279 | isPlaying = true; 280 | } 281 | } else { 282 | console.log("Model has no animations"); 283 | } 284 | 285 | // Remove loading overlay 286 | const loadingOverlay = document.querySelector(".loading-overlay"); 287 | if (loadingOverlay) { 288 | loadingOverlay.remove(); 289 | } 290 | }, 291 | function (xhr) { 292 | const percent = Math.floor((xhr.loaded / xhr.total) * 100); 293 | console.log(`Loading model: ${percent}% loaded`); 294 | }, 295 | function (error) { 296 | console.error("Error loading model:", error); 297 | console.error("Error details:", error.message); 298 | 299 | alert("Error loading 3D model - check browser console for details"); 300 | 301 | const loadingOverlay = document.querySelector(".loading-overlay"); 302 | if (loadingOverlay) { 303 | loadingOverlay.remove(); 304 | } 305 | } 306 | ); 307 | }) 308 | .catch((error) => { 309 | console.error("Error checking/loading scene.glb:", error); 310 | 311 | const loadingOverlay = document.querySelector(".loading-overlay"); 312 | if (loadingOverlay) { 313 | loadingOverlay.remove(); 314 | } 315 | 316 | const geometry = new THREE.BoxGeometry(1, 1, 1); 317 | const material = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 318 | const cube = new THREE.Mesh(geometry, material); 319 | scene.add(cube); 320 | 321 | const textDiv = document.createElement("div"); 322 | textDiv.style.position = "absolute"; 323 | textDiv.style.top = "50%"; 324 | textDiv.style.left = "50%"; 325 | textDiv.style.transform = "translate(-50%, -50%)"; 326 | textDiv.style.color = "#ffffff"; 327 | textDiv.style.background = "rgba(0,0,0,0.7)"; 328 | textDiv.style.padding = "20px"; 329 | textDiv.style.borderRadius = "5px"; 330 | textDiv.innerHTML = "Error loading model. See console for details."; 331 | viewer.appendChild(textDiv); 332 | }); 333 | } 334 | 335 | // Center camera on model 336 | function centerCameraOnModel(model) { 337 | const boundingBox = new THREE.Box3().setFromObject(model); 338 | const center = new THREE.Vector3(); 339 | boundingBox.getCenter(center); 340 | const size = new THREE.Vector3(); 341 | boundingBox.getSize(size); 342 | 343 | const maxDim = Math.max(size.x, size.y, size.z); 344 | const fov = camera.fov * (Math.PI / 180); 345 | let distance = maxDim / (2 * Math.tan(fov / 2)); 346 | distance = Math.max(distance, 1); 347 | 348 | if (controls) { 349 | const direction = camera.position.clone().sub(controls.target).normalize(); 350 | camera.position.copy( 351 | center.clone().add(direction.multiplyScalar(distance * 1.5)) 352 | ); 353 | controls.target.copy(center); 354 | controls.update(); 355 | } else { 356 | camera.position.set(center.x, center.y + distance, center.z + distance); 357 | camera.lookAt(center); 358 | } 359 | } 360 | 361 | // Window resize handler 362 | function onWindowResize() { 363 | camera.aspect = viewer.clientWidth / viewer.clientHeight; 364 | camera.updateProjectionMatrix(); 365 | renderer.setSize(viewer.clientWidth, viewer.clientHeight); 366 | } 367 | 368 | // Animation loop 369 | function animate() { 370 | requestAnimationFrame(animate); 371 | 372 | if (controls && controls.update) { 373 | controls.update(); 374 | } 375 | 376 | if (mixer && isPlaying) { 377 | const delta = clock.getDelta(); 378 | mixer.update(delta); 379 | 380 | if (animationDuration > 0) { 381 | const time = mixer.time % animationDuration; 382 | animationSlider.value = time; 383 | updateAnimationTime(time); 384 | } 385 | } 386 | 387 | renderer.render(scene, camera); 388 | } 389 | 390 | // Update animation time display 391 | function updateAnimationTime(time) { 392 | if (animationDuration > 0) { 393 | const minutes = Math.floor(time / 60); 394 | const seconds = Math.floor(time % 60); 395 | const formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`; 396 | 397 | const durationMinutes = Math.floor(animationDuration / 60); 398 | const durationSeconds = Math.floor(animationDuration % 60); 399 | const formattedDuration = `${durationMinutes}:${durationSeconds 400 | .toString() 401 | .padStart(2, "0")}`; 402 | 403 | animationTime.textContent = `${formattedTime} / ${formattedDuration}`; 404 | } else { 405 | animationTime.textContent = "0:00 / 0:00"; 406 | } 407 | } 408 | 409 | // Set up event listeners 410 | function setupEventListeners() { 411 | try { 412 | // Camera views 413 | document 414 | .getElementById("btn-reset-camera") 415 | .addEventListener("click", () => { 416 | if (scene.children.length > 0) { 417 | centerCameraOnModel(scene); 418 | } 419 | }); 420 | 421 | document.getElementById("btn-top-view").addEventListener("click", () => { 422 | camera.position.set(0, 10, 0); 423 | if (controls) { 424 | controls.target.set(0, 0, 0); 425 | controls.update(); 426 | } else { 427 | camera.lookAt(0, 0, 0); 428 | } 429 | }); 430 | 431 | document.getElementById("btn-front-view").addEventListener("click", () => { 432 | camera.position.set(0, 0, 10); 433 | if (controls) { 434 | controls.target.set(0, 0, 0); 435 | controls.update(); 436 | } else { 437 | camera.lookAt(0, 0, 0); 438 | } 439 | }); 440 | 441 | document.getElementById("btn-side-view").addEventListener("click", () => { 442 | camera.position.set(10, 0, 0); 443 | if (controls) { 444 | controls.target.set(0, 0, 0); 445 | controls.update(); 446 | } else { 447 | camera.lookAt(0, 0, 0); 448 | } 449 | }); 450 | 451 | // Display toggles 452 | document 453 | .getElementById("toggle-wireframe") 454 | .addEventListener("change", (e) => { 455 | scene.traverse((child) => { 456 | if (child.isMesh) { 457 | child.material.wireframe = e.target.checked; 458 | } 459 | }); 460 | }); 461 | 462 | document.getElementById("toggle-grid").addEventListener("change", (e) => { 463 | grid.visible = e.target.checked; 464 | }); 465 | 466 | document.getElementById("toggle-lights").addEventListener("change", (e) => { 467 | scene.traverse((child) => { 468 | if (child.isLight && !(child instanceof THREE.AmbientLight)) { 469 | child.visible = e.target.checked; 470 | } 471 | }); 472 | }); 473 | 474 | // Animation controls 475 | document.getElementById("btn-play").addEventListener("click", () => { 476 | if (mixer && animationActions.length > 0) { 477 | isPlaying = true; 478 | animationActions.forEach((action) => { 479 | action.paused = false; 480 | action.play(); 481 | }); 482 | } 483 | }); 484 | 485 | document.getElementById("btn-pause").addEventListener("click", () => { 486 | if (mixer && animationActions.length > 0) { 487 | isPlaying = false; 488 | animationActions.forEach((action) => { 489 | action.paused = true; 490 | }); 491 | } 492 | }); 493 | 494 | document.getElementById("btn-stop").addEventListener("click", () => { 495 | if (mixer && animationActions.length > 0) { 496 | isPlaying = false; 497 | animationActions.forEach((action) => { 498 | action.stop(); 499 | }); 500 | mixer.setTime(0); 501 | updateAnimationTime(0); 502 | } 503 | }); 504 | 505 | // Animation slider 506 | animationSlider.addEventListener("input", (e) => { 507 | if (mixer && animationActions.length > 0) { 508 | const time = parseFloat(e.target.value); 509 | mixer.setTime(time); 510 | updateAnimationTime(time); 511 | } 512 | }); 513 | } catch (e) { 514 | console.error("Error setting up event listeners:", e); 515 | } 516 | } 517 | 518 | // Initialize the viewer when the page loads 519 | window.addEventListener("load", () => { 520 | console.log("Window loaded, initializing viewer"); 521 | init(); 522 | setupEventListeners(); 523 | }); -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BlendXWeb | By Egret 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 88 | 89 | 90 | 91 |
92 | 93 |
94 | 95 |
96 |
97 |
Loading...
98 |
99 |
100 | 101 | 102 | 312 | 313 | 314 | 317 |
318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 520 | 521 | --------------------------------------------------------------------------------