meshvault 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meshvault-0.1.0/LICENSE +21 -0
- meshvault-0.1.0/PKG-INFO +158 -0
- meshvault-0.1.0/README.md +121 -0
- meshvault-0.1.0/backend/__init__.py +0 -0
- meshvault-0.1.0/backend/app.py +393 -0
- meshvault-0.1.0/backend/archive_inspector.py +508 -0
- meshvault-0.1.0/backend/export_manager.py +236 -0
- meshvault-0.1.0/backend/fbx_converter.py +502 -0
- meshvault-0.1.0/backend/file_browser.py +225 -0
- meshvault-0.1.0/frontend/css/styles.css +1003 -0
- meshvault-0.1.0/frontend/index.html +323 -0
- meshvault-0.1.0/frontend/js/app.js +520 -0
- meshvault-0.1.0/frontend/js/export_panel.js +152 -0
- meshvault-0.1.0/frontend/js/file_browser.js +383 -0
- meshvault-0.1.0/frontend/js/viewer_3d.js +1697 -0
- meshvault-0.1.0/pyproject.toml +55 -0
meshvault-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laurent-Philippe Albou (contact@abstractcore.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
meshvault-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meshvault
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A professional 3D asset browser for rapid browsing, previewing, and managing OBJ/FBX files
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: 3d,asset-browser,obj,fbx,three.js,viewer,meshvault
|
|
8
|
+
Author: Laurent-Philippe Albou
|
|
9
|
+
Author-email: contact@abstractcore.ai
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
25
|
+
Classifier: Topic :: Multimedia :: Graphics :: Viewers
|
|
26
|
+
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
27
|
+
Requires-Dist: fastapi (>=0.115.0,<0.116.0)
|
|
28
|
+
Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
|
|
29
|
+
Requires-Dist: rarfile (>=4.2,<5.0)
|
|
30
|
+
Requires-Dist: trimesh (>=4.4.0,<5.0.0)
|
|
31
|
+
Requires-Dist: uvicorn[standard] (>=0.30.0,<0.31.0)
|
|
32
|
+
Project-URL: Documentation, https://github.com/lpalbou/meshvault/tree/main/docs
|
|
33
|
+
Project-URL: Homepage, https://github.com/lpalbou/meshvault
|
|
34
|
+
Project-URL: Repository, https://github.com/lpalbou/meshvault
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# MeshVault
|
|
38
|
+
|
|
39
|
+
A professional, local web-based tool for rapidly browsing, previewing, and managing 3D assets (`.obj`, `.fbx`, `.gltf`, `.glb`, `.stl`) across your filesystem — including assets buried inside `.zip` and `.rar` archives.
|
|
40
|
+
|
|
41
|
+
[](https://github.com/lpalbou/meshvault/actions)
|
|
42
|
+
[](https://python.org)
|
|
43
|
+
[](https://fastapi.tiangolo.com)
|
|
44
|
+
[](https://threejs.org)
|
|
45
|
+
[](https://pypi.org/project/meshvault/)
|
|
46
|
+
[](https://www.npmjs.com/package/meshvault)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
| Feature | Description |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| **Folder Browsing** | Navigate your filesystem with a clean sidebar tree. Go up, go home, double-click to enter. List/grid view toggle. |
|
|
56
|
+
| **Search & Filter** | Real-time search input to filter folders and assets in the current directory. |
|
|
57
|
+
| **3D Asset Detection** | Finds `.obj`, `.fbx`, `.gltf`, `.glb`, `.stl` files — including inside `.zip` and `.rar` archives. |
|
|
58
|
+
| **Interactive 3D Viewer** | Click an asset to load it with high-quality PBR rendering, SSAO, soft shadows, tone mapping. |
|
|
59
|
+
| **Orbit / FPV Toggle** | Orbit mode (mouse orbit/zoom/pan, right-click pivot) and FPV drone mode (WASD fly, A/D yaw, mouse look). |
|
|
60
|
+
| **Viewer Toolbar** | Toggle grid, XYZ axes (colored + labeled), wireframe, and light controls from the top-right toolbar. |
|
|
61
|
+
| **Light Controls** | Adjustable key/fill/ambient intensity, light direction (azimuth/elevation), and exposure. |
|
|
62
|
+
| **Background Presets** | 12 background color swatches (dark, gray, light, tinted) for evaluating models on any backdrop. |
|
|
63
|
+
| **Model Transforms** | Center at origin, ground on Y=0, auto-orient via PCA, reset to original. All without moving the camera. |
|
|
64
|
+
| **Model Scaling** | Real-time scale slider (0.25×–2.0×). |
|
|
65
|
+
| **Modified Export** | Export applies all transforms (center, ground, orient, scale) — saves modified OBJ via Three.js OBJExporter. |
|
|
66
|
+
| **FBX Auto-Conversion** | Old FBX files (version < 7000) are auto-converted to OBJ via a built-in binary parser. |
|
|
67
|
+
| **Persistent Settings** | Scene settings (wireframe, grid, axes, background) persist across model loads. |
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
### Prerequisites
|
|
72
|
+
|
|
73
|
+
- **Python 3.10+**
|
|
74
|
+
- **Poetry** ([install guide](https://python-poetry.org/docs/#installation))
|
|
75
|
+
- For `.rar` support, one of: `bsdtar`, `unrar`, `7z`, or `unar` (auto-detected)
|
|
76
|
+
|
|
77
|
+
### Install & Run
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/lpalbou/meshvault.git
|
|
81
|
+
cd meshvault
|
|
82
|
+
poetry install --no-root
|
|
83
|
+
poetry run meshvault
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then open **http://localhost:8420** in your browser.
|
|
87
|
+
|
|
88
|
+
### Install from PyPI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install meshvault
|
|
92
|
+
meshvault
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Install from NPM
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx meshvault
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
1. **Browse**: Navigate folders in the sidebar. Toggle list/grid view. Filter by name.
|
|
104
|
+
2. **Preview**: Click any 3D asset to load it in the viewer (green=OBJ, orange=FBX, cyan=GLTF, purple=STL/archived).
|
|
105
|
+
3. **Navigate**: Orbit mode (left-drag orbit, scroll zoom, right-drag pan, right-click pivot) or FPV drone mode (W/Shift forward, S/Ctrl backward, A/D yaw, E/Q altitude). Spacebar resets camera.
|
|
106
|
+
4. **Scene tools**: Toggle grid, axes (XYZ), wireframe, and lighting from the toolbar. Pick background color from swatches.
|
|
107
|
+
5. **Transform**: Center model at origin, ground it on Y=0, or auto-orient via PCA. Reset undoes all transforms.
|
|
108
|
+
6. **Export**: Set name and path in the top bar, click Export. Modified models (centered/oriented/scaled) are exported as OBJ with baked transforms.
|
|
109
|
+
|
|
110
|
+
## Project Structure
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
meshvault/
|
|
114
|
+
├── backend/
|
|
115
|
+
│ ├── app.py # FastAPI server + routes
|
|
116
|
+
│ ├── file_browser.py # Filesystem navigation + asset discovery
|
|
117
|
+
│ ├── archive_inspector.py # ZIP/RAR inspection + multi-tool extraction
|
|
118
|
+
│ ├── export_manager.py # Asset export with renaming
|
|
119
|
+
│ └── fbx_converter.py # FBX 6100 binary parser + OBJ converter
|
|
120
|
+
├── frontend/
|
|
121
|
+
│ ├── index.html # Main HTML page
|
|
122
|
+
│ ├── css/styles.css # Dark professional theme
|
|
123
|
+
│ └── js/
|
|
124
|
+
│ ├── app.js # Main orchestrator
|
|
125
|
+
│ ├── file_browser.js # File browser + search + grid/list
|
|
126
|
+
│ ├── viewer_3d.js # Three.js 3D viewer
|
|
127
|
+
│ └── export_panel.js # Rename/export controls
|
|
128
|
+
├── tests/
|
|
129
|
+
│ └── test_file_browser.py # Backend unit tests
|
|
130
|
+
├── docs/ # Full documentation
|
|
131
|
+
├── pyproject.toml # Poetry / PyPI configuration
|
|
132
|
+
├── package.json # NPM configuration
|
|
133
|
+
└── poetry.lock
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Documentation
|
|
137
|
+
|
|
138
|
+
- [Getting Started](docs/getting_started.md) — Installation, first run, basic usage
|
|
139
|
+
- [Architecture](docs/architecture.md) — System design, components, design decisions
|
|
140
|
+
- [API Reference](docs/api.md) — REST API endpoints, request/response schemas
|
|
141
|
+
- [FAQ](docs/faq.md) — Common questions and troubleshooting
|
|
142
|
+
|
|
143
|
+
## Tests
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
poetry run pytest tests/ -v
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Contributing
|
|
150
|
+
|
|
151
|
+
Contributions are welcome! Please open an issue or submit a pull request on [GitHub](https://github.com/lpalbou/meshvault).
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
156
|
+
|
|
157
|
+
© 2026 Laurent-Philippe Albou — contact@abstractcore.ai
|
|
158
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# MeshVault
|
|
2
|
+
|
|
3
|
+
A professional, local web-based tool for rapidly browsing, previewing, and managing 3D assets (`.obj`, `.fbx`, `.gltf`, `.glb`, `.stl`) across your filesystem — including assets buried inside `.zip` and `.rar` archives.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/lpalbou/meshvault/actions)
|
|
6
|
+
[](https://python.org)
|
|
7
|
+
[](https://fastapi.tiangolo.com)
|
|
8
|
+
[](https://threejs.org)
|
|
9
|
+
[](https://pypi.org/project/meshvault/)
|
|
10
|
+
[](https://www.npmjs.com/package/meshvault)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
| Feature | Description |
|
|
18
|
+
|---------|-------------|
|
|
19
|
+
| **Folder Browsing** | Navigate your filesystem with a clean sidebar tree. Go up, go home, double-click to enter. List/grid view toggle. |
|
|
20
|
+
| **Search & Filter** | Real-time search input to filter folders and assets in the current directory. |
|
|
21
|
+
| **3D Asset Detection** | Finds `.obj`, `.fbx`, `.gltf`, `.glb`, `.stl` files — including inside `.zip` and `.rar` archives. |
|
|
22
|
+
| **Interactive 3D Viewer** | Click an asset to load it with high-quality PBR rendering, SSAO, soft shadows, tone mapping. |
|
|
23
|
+
| **Orbit / FPV Toggle** | Orbit mode (mouse orbit/zoom/pan, right-click pivot) and FPV drone mode (WASD fly, A/D yaw, mouse look). |
|
|
24
|
+
| **Viewer Toolbar** | Toggle grid, XYZ axes (colored + labeled), wireframe, and light controls from the top-right toolbar. |
|
|
25
|
+
| **Light Controls** | Adjustable key/fill/ambient intensity, light direction (azimuth/elevation), and exposure. |
|
|
26
|
+
| **Background Presets** | 12 background color swatches (dark, gray, light, tinted) for evaluating models on any backdrop. |
|
|
27
|
+
| **Model Transforms** | Center at origin, ground on Y=0, auto-orient via PCA, reset to original. All without moving the camera. |
|
|
28
|
+
| **Model Scaling** | Real-time scale slider (0.25×–2.0×). |
|
|
29
|
+
| **Modified Export** | Export applies all transforms (center, ground, orient, scale) — saves modified OBJ via Three.js OBJExporter. |
|
|
30
|
+
| **FBX Auto-Conversion** | Old FBX files (version < 7000) are auto-converted to OBJ via a built-in binary parser. |
|
|
31
|
+
| **Persistent Settings** | Scene settings (wireframe, grid, axes, background) persist across model loads. |
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- **Python 3.10+**
|
|
38
|
+
- **Poetry** ([install guide](https://python-poetry.org/docs/#installation))
|
|
39
|
+
- For `.rar` support, one of: `bsdtar`, `unrar`, `7z`, or `unar` (auto-detected)
|
|
40
|
+
|
|
41
|
+
### Install & Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/lpalbou/meshvault.git
|
|
45
|
+
cd meshvault
|
|
46
|
+
poetry install --no-root
|
|
47
|
+
poetry run meshvault
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then open **http://localhost:8420** in your browser.
|
|
51
|
+
|
|
52
|
+
### Install from PyPI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install meshvault
|
|
56
|
+
meshvault
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Install from NPM
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx meshvault
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
1. **Browse**: Navigate folders in the sidebar. Toggle list/grid view. Filter by name.
|
|
68
|
+
2. **Preview**: Click any 3D asset to load it in the viewer (green=OBJ, orange=FBX, cyan=GLTF, purple=STL/archived).
|
|
69
|
+
3. **Navigate**: Orbit mode (left-drag orbit, scroll zoom, right-drag pan, right-click pivot) or FPV drone mode (W/Shift forward, S/Ctrl backward, A/D yaw, E/Q altitude). Spacebar resets camera.
|
|
70
|
+
4. **Scene tools**: Toggle grid, axes (XYZ), wireframe, and lighting from the toolbar. Pick background color from swatches.
|
|
71
|
+
5. **Transform**: Center model at origin, ground it on Y=0, or auto-orient via PCA. Reset undoes all transforms.
|
|
72
|
+
6. **Export**: Set name and path in the top bar, click Export. Modified models (centered/oriented/scaled) are exported as OBJ with baked transforms.
|
|
73
|
+
|
|
74
|
+
## Project Structure
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
meshvault/
|
|
78
|
+
├── backend/
|
|
79
|
+
│ ├── app.py # FastAPI server + routes
|
|
80
|
+
│ ├── file_browser.py # Filesystem navigation + asset discovery
|
|
81
|
+
│ ├── archive_inspector.py # ZIP/RAR inspection + multi-tool extraction
|
|
82
|
+
│ ├── export_manager.py # Asset export with renaming
|
|
83
|
+
│ └── fbx_converter.py # FBX 6100 binary parser + OBJ converter
|
|
84
|
+
├── frontend/
|
|
85
|
+
│ ├── index.html # Main HTML page
|
|
86
|
+
│ ├── css/styles.css # Dark professional theme
|
|
87
|
+
│ └── js/
|
|
88
|
+
│ ├── app.js # Main orchestrator
|
|
89
|
+
│ ├── file_browser.js # File browser + search + grid/list
|
|
90
|
+
│ ├── viewer_3d.js # Three.js 3D viewer
|
|
91
|
+
│ └── export_panel.js # Rename/export controls
|
|
92
|
+
├── tests/
|
|
93
|
+
│ └── test_file_browser.py # Backend unit tests
|
|
94
|
+
├── docs/ # Full documentation
|
|
95
|
+
├── pyproject.toml # Poetry / PyPI configuration
|
|
96
|
+
├── package.json # NPM configuration
|
|
97
|
+
└── poetry.lock
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
- [Getting Started](docs/getting_started.md) — Installation, first run, basic usage
|
|
103
|
+
- [Architecture](docs/architecture.md) — System design, components, design decisions
|
|
104
|
+
- [API Reference](docs/api.md) — REST API endpoints, request/response schemas
|
|
105
|
+
- [FAQ](docs/faq.md) — Common questions and troubleshooting
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
poetry run pytest tests/ -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Contributions are welcome! Please open an issue or submit a pull request on [GitHub](https://github.com/lpalbou/meshvault).
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
120
|
+
|
|
121
|
+
© 2026 Laurent-Philippe Albou — contact@abstractcore.ai
|
|
File without changes
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main FastAPI application - serves the 3D asset browser.
|
|
3
|
+
|
|
4
|
+
This is the entry point that:
|
|
5
|
+
- Serves the frontend static files
|
|
6
|
+
- Provides REST API for file browsing, asset loading, and export
|
|
7
|
+
- Manages the lifecycle of backend services
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import mimetypes
|
|
13
|
+
import urllib.parse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
|
|
18
|
+
import uvicorn
|
|
19
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
20
|
+
from fastapi.staticfiles import StaticFiles
|
|
21
|
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from backend.file_browser import FileBrowser
|
|
25
|
+
from backend.archive_inspector import ArchiveInspector
|
|
26
|
+
from backend.export_manager import ExportManager
|
|
27
|
+
from backend.fbx_converter import get_fbx_version, convert_fbx_to_obj
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- Configuration ---
|
|
31
|
+
|
|
32
|
+
# Default browse root: user's home directory
|
|
33
|
+
DEFAULT_ROOT = str(Path.home())
|
|
34
|
+
|
|
35
|
+
# Register additional MIME types for 3D files
|
|
36
|
+
mimetypes.add_type("model/obj", ".obj")
|
|
37
|
+
mimetypes.add_type("model/fbx", ".fbx")
|
|
38
|
+
mimetypes.add_type("model/mtl", ".mtl")
|
|
39
|
+
mimetypes.add_type("model/gltf+json", ".gltf")
|
|
40
|
+
mimetypes.add_type("model/gltf-binary", ".glb")
|
|
41
|
+
mimetypes.add_type("model/stl", ".stl")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- Pydantic models for API ---
|
|
45
|
+
|
|
46
|
+
class ExportRequest(BaseModel):
|
|
47
|
+
"""Request body for exporting an asset."""
|
|
48
|
+
source_path: str
|
|
49
|
+
target_dir: str
|
|
50
|
+
new_name: str
|
|
51
|
+
is_in_archive: bool = False
|
|
52
|
+
archive_path: Optional[str] = None
|
|
53
|
+
inner_path: Optional[str] = None
|
|
54
|
+
related_files: list[str] = []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ExportModifiedRequest(BaseModel):
|
|
58
|
+
"""Request body for exporting a modified model (OBJ text from frontend)."""
|
|
59
|
+
target_dir: str
|
|
60
|
+
new_name: str
|
|
61
|
+
obj_content: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BrowseResponse(BaseModel):
|
|
65
|
+
"""Response for browse endpoint."""
|
|
66
|
+
current_path: str
|
|
67
|
+
parent_path: Optional[str]
|
|
68
|
+
folders: list[dict]
|
|
69
|
+
assets: list[dict]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --- App lifecycle ---
|
|
73
|
+
|
|
74
|
+
archive_inspector = ArchiveInspector()
|
|
75
|
+
file_browser = FileBrowser()
|
|
76
|
+
export_manager = ExportManager()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@asynccontextmanager
|
|
80
|
+
async def lifespan(app: FastAPI):
|
|
81
|
+
"""Manage application lifecycle — clean up temp files on shutdown."""
|
|
82
|
+
yield
|
|
83
|
+
archive_inspector.cleanup()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --- FastAPI App ---
|
|
87
|
+
|
|
88
|
+
app = FastAPI(
|
|
89
|
+
title="MeshVault",
|
|
90
|
+
description="Professional 3D asset browser for rapid management",
|
|
91
|
+
version="0.1.0",
|
|
92
|
+
lifespan=lifespan,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Serve frontend static files
|
|
96
|
+
# Works both in development (project root) and when installed via pip
|
|
97
|
+
# (frontend/ is installed alongside backend/ in site-packages parent)
|
|
98
|
+
_project_root = Path(__file__).parent.parent
|
|
99
|
+
frontend_dir = _project_root / "frontend"
|
|
100
|
+
if not frontend_dir.exists():
|
|
101
|
+
# Fallback: check if installed as a package (site-packages layout)
|
|
102
|
+
import importlib.resources
|
|
103
|
+
try:
|
|
104
|
+
frontend_dir = Path(importlib.resources.files("frontend"))
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
app.mount(
|
|
108
|
+
"/static",
|
|
109
|
+
StaticFiles(directory=str(frontend_dir)),
|
|
110
|
+
name="static",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- Routes ---
|
|
115
|
+
|
|
116
|
+
@app.get("/", response_class=HTMLResponse)
|
|
117
|
+
async def root():
|
|
118
|
+
"""Serve the main HTML page."""
|
|
119
|
+
index_path = frontend_dir / "index.html"
|
|
120
|
+
return HTMLResponse(content=index_path.read_text(encoding="utf-8"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.get("/api/browse")
|
|
124
|
+
async def browse(path: Optional[str] = Query(default=None)):
|
|
125
|
+
"""
|
|
126
|
+
Browse a directory and return its contents.
|
|
127
|
+
|
|
128
|
+
Query params:
|
|
129
|
+
path: Directory path to browse. Defaults to user's home.
|
|
130
|
+
"""
|
|
131
|
+
browse_path = path or DEFAULT_ROOT
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
result = file_browser.browse(browse_path)
|
|
135
|
+
except FileNotFoundError as e:
|
|
136
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
raise HTTPException(status_code=403, detail=str(e))
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"current_path": result.current_path,
|
|
142
|
+
"parent_path": result.parent_path,
|
|
143
|
+
"folders": [
|
|
144
|
+
{
|
|
145
|
+
"name": f.name,
|
|
146
|
+
"path": f.path,
|
|
147
|
+
"has_children": f.has_children,
|
|
148
|
+
}
|
|
149
|
+
for f in result.folders
|
|
150
|
+
],
|
|
151
|
+
"assets": [
|
|
152
|
+
{
|
|
153
|
+
"name": a.name,
|
|
154
|
+
"path": a.path,
|
|
155
|
+
"extension": a.extension,
|
|
156
|
+
"size": a.size,
|
|
157
|
+
"is_in_archive": a.is_in_archive,
|
|
158
|
+
"archive_path": a.archive_path,
|
|
159
|
+
"inner_path": a.inner_path,
|
|
160
|
+
"related_files": a.related_files,
|
|
161
|
+
}
|
|
162
|
+
for a in result.assets
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _maybe_convert_fbx(file_path: Path) -> tuple[Path, str]:
|
|
168
|
+
"""
|
|
169
|
+
Check if an FBX file needs conversion (version < 7000) and convert it.
|
|
170
|
+
|
|
171
|
+
Returns (path_to_serve, extension) — if converted, path points to the
|
|
172
|
+
generated OBJ file and extension is ".obj". Otherwise returns the
|
|
173
|
+
original path and extension unchanged.
|
|
174
|
+
"""
|
|
175
|
+
ext = file_path.suffix.lower()
|
|
176
|
+
if ext != ".fbx":
|
|
177
|
+
return file_path, ext
|
|
178
|
+
|
|
179
|
+
version = get_fbx_version(str(file_path))
|
|
180
|
+
if version is not None and version < 7000:
|
|
181
|
+
# FBX version too old for Three.js — convert to OBJ
|
|
182
|
+
obj_path = file_path.with_suffix(".converted.obj")
|
|
183
|
+
if not obj_path.exists():
|
|
184
|
+
success = convert_fbx_to_obj(str(file_path), str(obj_path))
|
|
185
|
+
if not success:
|
|
186
|
+
# Conversion failed — let the frontend try anyway
|
|
187
|
+
return file_path, ext
|
|
188
|
+
return obj_path, ".obj"
|
|
189
|
+
|
|
190
|
+
return file_path, ext
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.get("/api/asset/file")
|
|
194
|
+
async def serve_asset_file(path: str = Query(...)):
|
|
195
|
+
"""
|
|
196
|
+
Serve a 3D asset file for the viewer.
|
|
197
|
+
|
|
198
|
+
For regular files, serves directly.
|
|
199
|
+
For FBX files with version < 7000, auto-converts to OBJ.
|
|
200
|
+
"""
|
|
201
|
+
file_path = Path(path)
|
|
202
|
+
if file_path.exists() and file_path.is_file():
|
|
203
|
+
# Auto-convert old FBX if needed
|
|
204
|
+
serve_path, _ = _maybe_convert_fbx(file_path)
|
|
205
|
+
content_type = mimetypes.guess_type(str(serve_path))[0] or "application/octet-stream"
|
|
206
|
+
return FileResponse(
|
|
207
|
+
path=str(serve_path),
|
|
208
|
+
media_type=content_type,
|
|
209
|
+
filename=serve_path.name,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.get("/api/asset/archive")
|
|
216
|
+
async def serve_archive_asset(
|
|
217
|
+
archive_path: str = Query(...),
|
|
218
|
+
inner_path: str = Query(...),
|
|
219
|
+
):
|
|
220
|
+
"""
|
|
221
|
+
Extract and serve a 3D asset from an archive.
|
|
222
|
+
|
|
223
|
+
Extracts the asset (and related files) to a temp directory,
|
|
224
|
+
then serves the main asset file.
|
|
225
|
+
"""
|
|
226
|
+
extracted = archive_inspector.extract_asset(archive_path, inner_path)
|
|
227
|
+
if extracted is None:
|
|
228
|
+
raise HTTPException(
|
|
229
|
+
status_code=500,
|
|
230
|
+
detail=f"Failed to extract {inner_path} from {archive_path}",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
file_path = Path(extracted)
|
|
234
|
+
if not file_path.exists():
|
|
235
|
+
raise HTTPException(status_code=404, detail="Extracted file not found")
|
|
236
|
+
|
|
237
|
+
content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
|
|
238
|
+
return FileResponse(
|
|
239
|
+
path=str(file_path),
|
|
240
|
+
media_type=content_type,
|
|
241
|
+
filename=file_path.name,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.get("/api/asset/prepare_archive")
|
|
246
|
+
async def prepare_archive_asset(
|
|
247
|
+
archive_path: str = Query(...),
|
|
248
|
+
inner_path: str = Query(...),
|
|
249
|
+
):
|
|
250
|
+
"""
|
|
251
|
+
Extract an archived asset and return JSON with resolved temp paths.
|
|
252
|
+
|
|
253
|
+
This endpoint extracts the main asset and its related files to a
|
|
254
|
+
temp directory, then returns the absolute filesystem paths so the
|
|
255
|
+
frontend can use /api/asset/file and /api/asset/related with them.
|
|
256
|
+
|
|
257
|
+
This solves the problem of archive-internal paths not being valid
|
|
258
|
+
filesystem paths for the Three.js loaders.
|
|
259
|
+
"""
|
|
260
|
+
extracted = archive_inspector.extract_asset(archive_path, inner_path)
|
|
261
|
+
if extracted is None:
|
|
262
|
+
raise HTTPException(
|
|
263
|
+
status_code=500,
|
|
264
|
+
detail=f"Failed to extract {inner_path} from {archive_path}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
file_path = Path(extracted)
|
|
268
|
+
if not file_path.exists():
|
|
269
|
+
raise HTTPException(status_code=404, detail="Extracted file not found")
|
|
270
|
+
|
|
271
|
+
# Auto-convert old FBX if needed
|
|
272
|
+
serve_path, actual_ext = _maybe_convert_fbx(file_path)
|
|
273
|
+
|
|
274
|
+
# Build the file URL for the main asset (points to converted file if applicable)
|
|
275
|
+
file_url = f"/api/asset/file?path={urllib.parse.quote(str(serve_path))}"
|
|
276
|
+
|
|
277
|
+
# Resolve related file paths: map archive-internal -> extracted temp paths
|
|
278
|
+
# First, get all related files from the archive listing
|
|
279
|
+
result = file_browser.browse(str(Path(archive_path).parent))
|
|
280
|
+
archived_asset = None
|
|
281
|
+
for a in result.assets:
|
|
282
|
+
if (a.archive_path == archive_path and a.inner_path == inner_path):
|
|
283
|
+
archived_asset = a
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
related_inner = archived_asset.related_files if archived_asset else []
|
|
287
|
+
related_resolved = archive_inspector.get_extracted_related_paths(
|
|
288
|
+
archive_path, related_inner
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"file_url": file_url,
|
|
293
|
+
"file_path": str(serve_path),
|
|
294
|
+
"related_files": related_resolved,
|
|
295
|
+
# Tell frontend the actual format to use (may differ if converted)
|
|
296
|
+
"actual_extension": actual_ext,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@app.get("/api/asset/related")
|
|
301
|
+
async def serve_related_file(path: str = Query(...)):
|
|
302
|
+
"""
|
|
303
|
+
Serve a related file (texture, material) for the 3D viewer.
|
|
304
|
+
|
|
305
|
+
This endpoint allows the Three.js loaders to fetch .mtl files,
|
|
306
|
+
textures, etc., that are referenced by the main 3D asset.
|
|
307
|
+
"""
|
|
308
|
+
file_path = Path(path)
|
|
309
|
+
if not file_path.exists():
|
|
310
|
+
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
311
|
+
|
|
312
|
+
content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
|
|
313
|
+
return FileResponse(
|
|
314
|
+
path=str(file_path),
|
|
315
|
+
media_type=content_type,
|
|
316
|
+
filename=file_path.name,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.post("/api/export")
|
|
321
|
+
async def export_asset(request: ExportRequest):
|
|
322
|
+
"""
|
|
323
|
+
Export a 3D asset to a target directory with a new name.
|
|
324
|
+
|
|
325
|
+
Handles both regular files and archived assets.
|
|
326
|
+
"""
|
|
327
|
+
result = export_manager.export_asset(
|
|
328
|
+
source_path=request.source_path,
|
|
329
|
+
target_dir=request.target_dir,
|
|
330
|
+
new_name=request.new_name,
|
|
331
|
+
is_in_archive=request.is_in_archive,
|
|
332
|
+
archive_path=request.archive_path,
|
|
333
|
+
inner_path=request.inner_path,
|
|
334
|
+
related_files=request.related_files,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not result.success:
|
|
338
|
+
raise HTTPException(status_code=500, detail=result.message)
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"success": result.success,
|
|
342
|
+
"output_path": result.output_path,
|
|
343
|
+
"message": result.message,
|
|
344
|
+
"files_exported": result.files_exported,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@app.post("/api/export_modified")
|
|
349
|
+
async def export_modified(request: ExportModifiedRequest):
|
|
350
|
+
"""
|
|
351
|
+
Export a modified model (OBJ text generated by the frontend).
|
|
352
|
+
|
|
353
|
+
This is used when the user has recentered, auto-oriented, or scaled
|
|
354
|
+
the model in the viewer and wants to export the modified version.
|
|
355
|
+
"""
|
|
356
|
+
target_dir = Path(request.target_dir)
|
|
357
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
|
|
359
|
+
obj_path = target_dir / f"{request.new_name}.obj"
|
|
360
|
+
try:
|
|
361
|
+
obj_path.write_text(request.obj_content, encoding="utf-8")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
raise HTTPException(status_code=500, detail=f"Failed to write: {e}")
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
"success": True,
|
|
367
|
+
"output_path": str(target_dir),
|
|
368
|
+
"message": f"Exported modified model as OBJ",
|
|
369
|
+
"files_exported": [str(obj_path)],
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@app.get("/api/default_path")
|
|
374
|
+
async def get_default_path():
|
|
375
|
+
"""Return the default browse path (user home)."""
|
|
376
|
+
return {"path": DEFAULT_ROOT}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def main():
|
|
380
|
+
"""Entry point for running the server."""
|
|
381
|
+
port = int(os.environ.get("PORT", 8420))
|
|
382
|
+
print(f"\n 🎨 MeshVault")
|
|
383
|
+
print(f" → Open http://localhost:{port} in your browser\n")
|
|
384
|
+
uvicorn.run(
|
|
385
|
+
"backend.app:app",
|
|
386
|
+
host="0.0.0.0",
|
|
387
|
+
port=port,
|
|
388
|
+
reload=False,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
main()
|