Compare commits

...

10 Commits

Author SHA1 Message Date
cjw e63d81f0aa Merge branch 'main' of http://jymj.top:10001/cjw/geMoldInsight 2026-02-14 17:15:22 +08:00
cjw 2c5d840117 init x 2026-02-13 01:01:20 +08:00
cjw d95d8610b4 init x 2026-02-13 00:42:20 +08:00
cjw f52006554c init 2026-02-13 00:35:03 +08:00
cjw aa1c91b818 init 2026-02-13 00:29:12 +08:00
cjw 09e586734b init 2026-02-13 00:04:54 +08:00
cjw c823f554a1 Remove venv from git 2026-02-13 00:03:28 +08:00
cjw 89660bba4e init 2026-02-12 23:27:48 +08:00
cjw 7b09eb3d89 init 2026-02-12 23:27:42 +08:00
cjw 96000f3f11 Initial commit 2026-02-12 23:17:53 +08:00
70 changed files with 37701 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
# 服务配置
HOST=0.0.0.0
# ================================
# 端口配置 - 唯一修改端口的地方
# ================================
# 应用端口(容器内端口)
PORT=10003
# Docker映射到宿主机的端口(docker-compose使用)
HOST_PORT=10001
# ================================
DEBUG=false
# 文件处理配置
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=104857600
ALLOWED_EXTENSIONS=.stp,.step,.stp.gz
# 几何处理配置
POINTCLOUD_SAMPLE_COUNT=10000
MESH_QUALITY=high
PARALLEL_PROCESSING=true
# 数据库配置
DB_HOST=szcjw
DB_PORT=5432
DB_NAME=moldinsight
DB_USER=moldinsight
DB_PASSWORD=Qqs1996*
# RustFS 对象存储配置 (S3v4 API)
RUSTFS_ENDPOINT=http://szcjw:9000
RUSTFS_ACCESS_KEY=1RlKXw7v3DAsFr4fLckt
RUSTFS_SECRET_KEY=KjWCHXZOh7GAtkLq0eQgNpMSmE6zw8Ddyiou21bB
RUSTFS_TIMEOUT=30
RUSTFS_PRESIGNED_URL_EXPIRES=3600
# JWT认证配置
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
+42
View File
@@ -0,0 +1,42 @@
# 服务配置
HOST=0.0.0.0
# ================================
# 端口配置 - 唯一修改端口的地方
# ================================
# 应用端口(容器内端口)
PORT=8000
# Docker映射到宿主机的端口(docker-compose使用)
HOST_PORT=10001
# ================================
DEBUG=false
# 文件处理配置
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=104857600
ALLOWED_EXTENSIONS=.stp,.step,.stp.gz
# 几何处理配置
POINTCLOUD_SAMPLE_COUNT=10000
MESH_QUALITY=high
PARALLEL_PROCESSING=true
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=moldinsight
DB_USER=moldinsight_user
DB_PASSWORD=moldinsight_password
# RustFS 对象存储配置 (S3v4 API)
RUSTFS_ENDPOINT=http://localhost:9000
RUSTFS_ACCESS_KEY=your-access-key
RUSTFS_SECRET_KEY=your-secret-key
RUSTFS_TIMEOUT=30
RUSTFS_PRESIGNED_URL_EXPIRES=3600
# JWT认证配置
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
+19
View File
@@ -0,0 +1,19 @@
# Python 虚拟环境
venv/
env/
.env
.venv/
# Python 缓存文件
__pycache__/
*.pyc
*.pyo
*.pyd
# IDE 配置(可选)
.idea/
.vscode/
# 项目临时文件
.DS_Store
*.log
+1
View File
@@ -0,0 +1 @@
+170
View File
@@ -0,0 +1,170 @@
# 部署时端口配置说明
## 生产环境部署端口配置
本项目现在使用统一的端口配置文件 `.env`,部署时需要相应调整配置。
### Gunicorn 启动配置
**推荐方式(使用 .env 配置):**
```bash
# 创建启动脚本
cat > start_production.sh << 'EOF'
#!/bin/bash
cd /opt/moldinsight/moldinsight_project
source venv/bin/activate
# 从 .env 读取端口配置
if [ -f .env ]; then
PORT=$(grep '^PORT=' .env | cut -d'=' -f2)
else
PORT=8000
fi
# 启动服务
gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${PORT}
EOF
chmod +x start_production.sh
./start_production.sh
```
### systemd 服务配置
创建 `/etc/systemd/system/moldinsight.service`:
```ini
[Unit]
Description=MoldInsight Geometry Analysis Service
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/moldinsight/moldinsight_project
Environment=PATH=/opt/moldinsight/moldinsight_project/venv/bin
Environment="PORT=8000"
ExecStart=/opt/moldinsight/moldinsight_project/venv/bin/gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${PORT}
Restart=always
[Install]
WantedBy=multi-user.target
```
**重要:** 在 `[Service]` 部分添加 `Environment="PORT=8000"`,或在 `.env` 文件中配置 `PORT=8000`。
### Nginx 反向代理配置
```nginx
upstream moldinsight_backend {
server 127.0.0.1:8000; # 对应 .env 中的 PORT
}
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://moldinsight_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static {
alias /opt/moldinsight/moldinsight_project/static;
}
location /html_output {
alias /opt/moldinsight/moldinsight_project/html_output;
}
}
```
### Docker 部署
Docker 部署自动从 `.env` 读取配置,无需额外设置:
```bash
# .env 文件配置
PORT=8000 # 容器内端口
HOST_PORT=8080 # 宿主机端口
# 启动
docker-compose up -d
```
### 修改生产环境端口
1. **编辑 .env 文件**
```bash
PORT=9000 # 修改应用端口
```
2. **重启服务**
```bash
# systemd
sudo systemctl restart moldinsight
# Docker
docker-compose down && docker-compose up -d
# 手动启动
./start_production.sh
```
3. **更新 Nginx 配置(如果使用)**
```nginx
upstream moldinsight_backend {
server 127.0.0.1:9000; # 更新为新端口
}
```
```bash
sudo nginx -t && sudo nginx -s reload
```
### 防火墙配置
如果修改了端口,需要更新防火墙规则:
```bash
# UFW (Ubuntu/Debian)
sudo ufw allow 9000/tcp
sudo ufw delete allow 8000/tcp # 删除旧端口
# firewall-cmd (CentOS/RHEL)
sudo firewall-cmd --permanent --add-port=9000/tcp
sudo firewall-cmd --permanent --remove-port=8000/tcp
sudo firewall-cmd --reload
```
### 健康检查
修改端口后,更新健康检查命令:
```bash
# 检查服务状态
curl http://localhost:9000/health
curl http://your-domain.com/health
```
## 快速参考
| 部署方式 | 端口配置位置 | 重启命令 |
|---------|------------|---------|
| 直接运行 | `.env` 中的 `PORT` | Ctrl+C 后重新运行 |
| Gunicorn | `.env` 中的 `PORT` | `systemctl restart moldinsight` |
| Docker | `.env` 中的 `PORT` 和 `HOST_PORT` | `docker-compose restart` |
| Nginx代理 | Nginx配置中的 `proxy_pass` | `nginx -s reload` |
## 注意事项
⚠️ **重要:**
1. 所有端口配置统一在 `.env` 文件中管理
2. 修改端口后需要同步更新相关配置(Nginx、防火墙等)
3. 确保新端口没有被其他服务占用
4. 生产环境建议使用 Nginx 反向代理,对外提供 80/443 端口
5. .env 文件不应提交到版本控制系统,使用 `.env.example` 作为模板
+29
View File
@@ -0,0 +1,29 @@
# Dockerfile for MoldInsight
FROM continuumio/miniconda3:latest
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY . .
# 更新conda并创建环境
RUN conda update -n base -c defaults conda -y && \
conda create -n moldinsight python=3.11 pythonocc-core=7.9.0 -c conda-forge -y
# 激活环境并安装Python依赖
RUN . /opt/conda/etc/profile.d/conda.sh && \
conda activate moldinsight && \
pip install -r requirements.txt
# 创建必要的目录
RUN mkdir -p uploads html_output logs
# 设置启动脚本
RUN chmod +x start.sh
# 暴露端口
EXPOSE 8000
# 启动命令(使用shell形式确保环境激活)
CMD ["/bin/bash", "-c", "source /opt/conda/etc/profile.d/conda.sh && conda activate moldinsight && python src/main.py"]
+281
View File
@@ -0,0 +1,281 @@
# MoldInsight Linux 部署指南
## 系统要求
- Linux 系统 (Ubuntu 20.04+ / CentOS 8+)
- Python 3.8+
- PostgreSQL 12+
- Git
## 1. 环境准备
### 安装系统依赖
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install python3 python3-pip python3-venv postgresql postgresql-contrib git
# CentOS/RHEL
sudo yum update
sudo yum install python3 python3-pip postgresql postgresql-server git
```
### 配置PostgreSQL
```bash
# 启动PostgreSQL服务
sudo systemctl start postgresql
sudo systemctl enable postgresql
# 创建数据库和用户
sudo -u postgres psql
```
在PostgreSQL中执行:
```sql
CREATE DATABASE moldinsight;
CREATE USER molduser WITH PASSWORD 'moldpassword';
GRANT ALL PRIVILEGES ON DATABASE moldinsight TO molduser;
\q
```
## 2. 项目部署
### 克隆或复制项目
```bash
# 如果使用Git
cd /opt
sudo git clone <your-repo-url> moldinsight
sudo chown -R $USER:$USER moldinsight
cd moldinsight
# 或者直接复制项目文件到Linux服务器
```
### 创建Python虚拟环境
```bash
cd moldinsight_project
python3 -m venv venv
source venv/bin/activate
```
### 安装依赖
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
## 3. 环境配置
### 修改环境配置文件
编辑 `.env` 文件:
```bash
nano .env
```
修改为Linux环境的配置:
```env
# 数据库配置(Linux环境)
DATABASE_URL=postgresql+asyncpg://molduser:moldpassword@localhost:5432/moldinsight
# 服务配置
DEBUG=false
HOST=0.0.0.0
PORT=8000
# Redis配置(可选)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# Kafka配置(可选)
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
KAFKA_SECURITY_PROTOCOL=PLAINTEXT
```
### 创建必要的目录
```bash
mkdir -p uploads html_output logs
chmod 755 uploads html_output logs
```
## 4. 启动服务
### 开发模式启动
```bash
cd moldinsight_project
source venv/bin/activate
python src/main.py
```
### 生产环境启动(使用Gunicorn)
```bash
# 安装Gunicorn
pip install gunicorn uvloop httptools
# 启动服务
cd moldinsight_project
source venv/bin/activate
gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
```
## 5. 系统服务配置(可选)
### 创建systemd服务文件
```bash
sudo nano /etc/systemd/system/moldinsight.service
```
添加以下内容:
```ini
[Unit]
Description=MoldInsight Geometry Analysis Service
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/moldinsight/moldinsight_project
Environment=PATH=/opt/moldinsight/moldinsight_project/venv/bin
ExecStart=/opt/moldinsight/moldinsight_project/venv/bin/gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Restart=always
[Install]
WantedBy=multi-user.target
```
### 启用并启动服务
```bash
sudo systemctl daemon-reload
sudo systemctl enable moldinsight
sudo systemctl start moldinsight
sudo systemctl status moldinsight
```
## 6. Nginx反向代理配置(可选)
### 安装Nginx
```bash
# Ubuntu/Debian
sudo apt install nginx
# CentOS/RHEL
sudo yum install nginx
```
### 创建Nginx配置文件
```bash
sudo nano /etc/nginx/sites-available/moldinsight
```
添加以下内容:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static {
alias /opt/moldinsight/moldinsight_project/static;
expires 30d;
}
}
```
### 启用站点并重启Nginx
```bash
sudo ln -s /etc/nginx/sites-available/moldinsight /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
## 7. 防火墙配置
```bash
# Ubuntu/Debian (ufw)
sudo ufw allow 80
sudo ufw allow 8000
sudo ufw allow ssh
sudo ufw enable
# CentOS/RHEL (firewalld)
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=8000/tcp
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
```
## 8. 验证部署
### 检查服务状态
```bash
# 检查应用服务
curl http://localhost:8000/health
# 检查数据库连接
sudo -u postgres psql -d moldinsight -c "SELECT version();"
```
### 测试文件上传
访问 `http://your-server-ip:8000` 上传STP文件测试功能。
## 9. 故障排除
### 常见问题
1. **数据库连接失败**
- 检查PostgreSQL服务状态:`sudo systemctl status postgresql`
- 验证数据库连接:`psql -h localhost -U molduser -d moldinsight`
2. **端口被占用**
- 检查端口使用:`netstat -tulpn | grep 8000`
- 修改端口或停止占用进程
3. **权限问题**
- 确保目录权限正确:`chmod 755 uploads html_output logs`
- 检查文件所有者:`ls -la`
4. **依赖安装失败**
- 更新pip:`pip install --upgrade pip`
- 使用国内镜像:`pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
## 10. 备份和恢复
### 数据库备份
```bash
# 备份数据库
sudo -u postgres pg_dump moldinsight > moldinsight_backup.sql
# 恢复数据库
sudo -u postgres psql -d moldinsight < moldinsight_backup.sql
```
### 文件备份
```bash
# 备份上传的文件和配置
tar -czf moldinsight_backup.tar.gz uploads/ html_output/ .env requirements.txt
```
## 快速启动脚本
创建启动脚本 `start.sh`:
```bash
#!/bin/bash
cd /opt/moldinsight/moldinsight_project
source venv/bin/activate
python src/main.py
```
赋予执行权限:
```bash
chmod +x start.sh
./start.sh
```
现在您的MoldInsight项目已经可以在Linux环境下正常运行!
+107
View File
@@ -0,0 +1,107 @@
# 端口配置说明
## ⚠️ 重要提示
本项目现在采用**统一端口配置管理**,所有端口配置集中在一个地方:
**唯一修改端口的地方:** `.env` 文件中的端口配置部分
## 配置说明
编辑项目根目录下的 `.env` 文件:
```bash
# ================================
# 端口配置 - 唯一修改端口的地方
# ================================
# 应用端口(容器内端口)
PORT=8000
# Docker映射到宿主机的端口(docker-compose使用)
HOST_PORT=10001
# ================================
```
### 端口含义
| 变量 | 用途 | 默认值 | 说明 |
|------|------|--------|------|
| `PORT` | 应用监听端口 | 8000 | FastAPI/Uvicorn 服务监听的端口 |
| `HOST_PORT` | 宿主机映射端口 | 10001 | Docker Compose 映射到宿主机的端口 |
### 使用场景
#### 1. 本地直接运行(Python)
```bash
python src/main.py
```
服务将在 `http://localhost:8000` 启动(使用 `PORT` 配置)
#### 2. Docker Compose 运行
```bash
docker-compose up
```
服务将在 `http://localhost:10001` 访问(使用 `HOST_PORT` 配置)
容器内部使用 `PORT` 配置的端口(8000)
#### 3. 修改端口
**场景A:只想修改外部访问端口(Docker)**
```bash
# .env 文件
PORT=8000 # 容器内不变
HOST_PORT=8080 # 宿主机改为8080
```
访问地址:`http://localhost:8080`
**场景B:修改应用端口(容器内/本地运行)**
```bash
# .env 文件
PORT=9000 # 应用改为9000
HOST_PORT=10001 # 宿主机映射到10001
```
- 本地运行:`http://localhost:9000`
- Docker运行:`http://localhost:10001` (映射到容器内9000)
**场景C:同时修改两个端口**
```bash
# .env 文件
PORT=9000
HOST_PORT=9000
```
- 本地运行:`http://localhost:9000`
- Docker运行:`http://localhost:9000`
## 配置文件说明
### 配置读取优先级
1. **`config/settings.py`** - 从 `.env` 读取 `PORT` 和 `HOST`
2. **`src/main.py`** - 从 `settings` 获取端口配置
3. **`docker-compose.yml`** - 从 `.env` 读取 `HOST_PORT` 和 `PORT`
### 相关文件
- **`.env`** - ⭐ 唯一需要修改的配置文件
- **`config/settings.py`** - 配置读取逻辑(无需修改)
- **`src/main.py`** - 使用配置启动服务(无需修改)
- **`docker-compose.yml`** - Docker端口映射(自动读取 `.env`)
- **`start.sh` / `start_fixed.sh`** - 启动脚本(自动读取 `.env`)
## 常见问题
### Q: 为什么 Docker 宿主机端口和应用端口分开配置?
A: 这样可以灵活调整容器端口而不影响外部访问,也避免端口冲突。
### Q: 修改后需要重启吗?
A: 是的,修改 `.env` 后需要重启服务才能生效:
- 本地运行:Ctrl+C 停止后重新 `python src/main.py`
- Docker: `docker-compose down && docker-compose up`
### Q: 如何避免端口冲突?
A: 确保 `HOST_PORT` 不与其他服务冲突,可以使用 `netstat -an | grep <端口>` 检查端口占用情况。
### Q: 可以使用 80 端口吗?
A: 可以,但需要管理员权限:
- Linux/Mac: 使用 sudo
- Docker: 需要容器有足够权限
- 生产环境建议使用反向代理(如 Nginx)
+192
View File
@@ -0,0 +1,192 @@
# 端口配置重构总结
## 修改内容
本次重构将项目的端口配置统一到 `.env` 文件中,确保整个项目只有一个地方需要修改端口。
## 修改的文件
### 1. ⭐ `.env` - 唯一配置入口
**变更:** 添加了统一的端口配置区域
```bash
# ================================
# 端口配置 - 唯一修改端口的地方
# ================================
# 应用端口(容器内端口)
PORT=8000
# Docker映射到宿主机的端口(docker-compose使用)
HOST_PORT=10001
# ================================
```
### 2. `src/main.py`
**变更:** 从硬编码的环境变量读取改为从 `config.settings` 读取
```python
# 修改前
host = os.getenv('HOST', '0.0.0.0')
port = int(os.getenv('PORT', '8000'))
# 修改后
from config.settings import settings
# ...
host=settings.HOST,
port=settings.PORT
```
### 3. `docker-compose.yml`
**变更:** 端口映射从硬编码改为从环境变量读取
```yaml
# 修改前
ports:
- "10001:8000"
environment:
- PORT=8000
# 修改后
ports:
- "${HOST_PORT:-10001}:${CONTAINER_PORT:-8000}"
environment:
- HOST=${HOST:-0.0.0.0}
- PORT=${CONTAINER_PORT:-8000}
```
### 4. `start.sh` 和 `start_fixed.sh`
**变更:** 自动从 `.env` 读取端口并显示正确的访问地址
```bash
# 添加
PORT=$(grep '^PORT=' .env 2>/dev/null | cut -d'=' -f2 || echo '8000')
echo "🌐 服务将在 http://localhost:${PORT} 启动"
```
### 5. `README.md`
**变更:** 更新访问说明,提示端口配置位置
### 6. 新增文件
- `.env.example` - 配置文件模板
- `PORT_CONFIG.md` - 端口配置详细说明
- `DEPLOY_PORT.md` - 部署时端口配置指南
## 配置读取流程
```
.env 文件
↓
config/settings.py (读取 PORT 和 HOST)
↓
src/main.py (使用 settings.PORT)
↓
uvicorn 启动服务
```
Docker 部署流程:
```
.env 文件
↓
docker-compose.yml (读取 HOST_PORT 和 PORT)
↓
容器映射和内部启动
```
## 如何修改端口
### 方法 1:修改应用端口
```bash
# 编辑 .env
PORT=9000 # 修改此行
```
- 本地运行:`http://localhost:9000`
- Docker运行:需同时修改 `HOST_PORT=9000`
### 方法 2:修改 Docker 外部访问端口
```bash
# 编辑 .env
HOST_PORT=8080 # 修改此行(PORT 保持不变)
```
- Docker运行:`http://localhost:8080`
- 容器内仍使用 PORT 配置的端口
## 测试验证
### 测试 1:本地运行
```bash
# 修改 .env 中的 PORT
PORT=9999
# 启动服务
python src/main.py
# 验证
curl http://localhost:9999/health
```
### 测试 2:Docker 运行
```bash
# 修改 .env
PORT=8000
HOST_PORT=9999
# 启动容器
docker-compose up -d
# 验证
curl http://localhost:9999/health
```
### 测试 3:启动脚本
```bash
# 修改 .env
PORT=8888
# 运行启动脚本
./start.sh
# 检查输出是否显示正确的端口
```
## 注意事项
1. ✅ 所有端口配置集中在 `.env` 文件
2. ✅ 无需修改代码文件即可更改端口
3. ✅ 支持本地运行和 Docker 部署两种场景
4. ✅ 提供了详细的配置文档
5. ⚠️ 修改端口后需要重启服务
6. ⚠️ Docker 部署时需要同时考虑容器内外端口
7. ⚠️ 确保新端口没有被占用
## 文件清单
### 修改的文件
- `.env` - 添加端口配置区域
- `src/main.py` - 统一使用 settings 配置
- `docker-compose.yml` - 支持环境变量配置端口
- `start.sh` - 自动读取和显示端口
- `start_fixed.sh` - 自动读取和显示端口
- `README.md` - 更新访问说明
### 新增的文件
- `.env.example` - 配置模板
- `PORT_CONFIG.md` - 端口配置详细说明
- `DEPLOY_PORT.md` - 部署配置指南
- `PORT_REFACTOR_SUMMARY.md` - 本文档
## 回滚方案
如果需要回滚,按以下步骤操作:
```bash
git checkout -- src/main.py
git checkout -- docker-compose.yml
git checkout -- start.sh start_fixed.sh
git checkout -- .env
git checkout -- README.md
# 删除新增文件
rm .env.example PORT_CONFIG.md DEPLOY_PORT.md PORT_REFACTOR_SUMMARY.md
```
## 联系支持
如有问题,请查看:
- `PORT_CONFIG.md` - 端口配置详细说明
- `DEPLOY_PORT.md` - 部署配置指南
- `.env.example` - 配置示例
+76 -1
View File
@@ -1,2 +1,77 @@
# geMoldInsight
# MoldInsight - 模具几何分析系统
## 项目结构
```
moldinsight_project/
├── src/ # 源代码目录
│ ├── main.py # 主程序入口
│ ├── api/ # API路由
│ │ └── routes.py
│ ├── core/ # 核心业务逻辑
│ │ ├── stp_parser.py
│ │ ├── geometry_analyzer.py
│ │ └── mesh_generator.py
│ ├── models/ # 数据模型
│ │ ├── schemas.py
│ │ └── database.py
│ ├── services/ # 业务服务
│ │ └── storage_service.py
│ ├── database/ # 数据库管理
│ │ ├── database.py
│ │ └── init_db.py
│ └── utils/ # 工具类
│ ├── logger.py
│ ├── file_handler.py
│ └── html_generator.py
├── config/ # 配置文件
│ └── settings.py
├── static/ # 静态文件
│ ├── script.js
│ └── style.css
├── templates/ # HTML模板
│ └── index.html
├── uploads/ # 上传文件目录
├── html_output/ # 生成的HTML文件
├── logs/ # 日志文件
├── tests/ # 测试文件
├── docs/ # 文档
├── requirements.txt # 依赖包
├── docker-compose.yml # Docker配置
└── .env # 环境变量
```
## 功能特性
- ✅ STP文件解析和几何分析
- ✅ JSON数据导出
- ✅ PostgreSQL数据库存储
- ✅ 3D可视化HTML生成
- ✅ Web界面文件上传
- ✅ 任务状态跟踪
## 这是一个模具设计项目:
- 输入: 产品的三维模型(STP文件)
- 输出: 模具设计方案(型腔、型芯、工艺参数等)
- 目标: 为泡沫产品(ABS、PP、PC等材料)设计铝制模具
## 快速开始
1. 安装依赖:
```bash
pip install -r requirements.txt
```
2. 配置数据库连接(修改.env文件)
3. 启动服务:
```bash
python src/main.py
```
4. 访问服务
- 查看端口配置:打开 `.env` 文件查看 `PORT` 的值
- 默认端口:8000
- 访问地址:http://localhost:${PORT}
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
RustFS 旧桶清理脚本
删除所有遗留的旧桶结构,包括:
- moldinsight-geometry
- moldinsight-stp-files
- moldinsight-mold-cavities
- moldinsight-html
- moldinsight-user-files
注意:这是rustFS,而不是minio,只是用了minio的通用S3接口
"""
import asyncio
import sys
import os
from pathlib import Path
from typing import List, Dict
from datetime import datetime
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent
src_root = project_root / "src"
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from storage.rustfs_storage import RustFSManager
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class RustFSCleanup:
"""RustFS 旧桶清理器"""
def __init__(self):
self.rustfs = RustFSManager()
self.is_connected = False
# 所有需要清理的旧桶
self.old_buckets_to_clean = [
'moldinsight-geometry',
'moldinsight-stp-files',
'moldinsight-mold-cavities',
'moldinsight-html',
'moldinsight-user-files'
]
async def connect(self):
"""连接到 RustFS"""
try:
await self.rustfs.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
self.is_connected = True
logger.info("RustFS 连接成功")
return True
except Exception as e:
logger.error(f"RustFS 连接失败: {e}")
return False
async def close(self):
"""关闭连接"""
await self.rustfs.close()
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def list_all_buckets(self) -> List[str]:
"""列出所有桶"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
buckets = self.rustfs.client.list_buckets()
bucket_names = [bucket.name for bucket in buckets]
logger.info(f"当前存在的桶: {bucket_names}")
return bucket_names
except Exception as e:
logger.error(f"列出桶失败: {e}")
return []
async def check_bucket_status(self) -> Dict[str, Dict]:
"""检查桶状态和文件数量"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
bucket_status = {}
for bucket_name in self.old_buckets_to_clean:
try:
# 检查桶是否存在
exists = self.rustfs.client.bucket_exists(bucket_name)
if exists:
# 统计文件数量
objects = list(self.rustfs.client.list_objects(bucket_name, recursive=True))
file_count = len(objects)
# 计算总大小
total_size = sum(obj.size for obj in objects)
bucket_status[bucket_name] = {
'exists': True,
'file_count': file_count,
'total_size': total_size,
'files': [obj.object_name for obj in objects]
}
logger.info(f"桶 {bucket_name}: 存在, {file_count} 个文件, {total_size} 字节")
else:
bucket_status[bucket_name] = {
'exists': False,
'file_count': 0,
'total_size': 0,
'files': []
}
logger.info(f"桶 {bucket_name}: 不存在")
except Exception as e:
logger.error(f"检查桶 {bucket_name} 状态失败: {e}")
bucket_status[bucket_name] = {
'exists': False,
'file_count': 0,
'total_size': 0,
'files': [],
'error': str(e)
}
return bucket_status
async def delete_bucket(self, bucket_name: str) -> Dict[str, any]:
"""删除单个桶及其所有文件"""
result = {
'bucket_name': bucket_name,
'success': False,
'files_deleted': 0,
'error': None
}
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(bucket_name):
result['success'] = True
result['message'] = "桶不存在,无需删除"
logger.info(f"桶 {bucket_name} 不存在,跳过删除")
return result
# 列出所有文件
objects = list(self.rustfs.client.list_objects(bucket_name, recursive=True))
file_count = len(objects)
if file_count > 0:
logger.info(f"开始删除桶 {bucket_name} 中的 {file_count} 个文件")
# 删除所有文件
for obj in objects:
try:
self.rustfs.client.remove_object(bucket_name, obj.object_name)
result['files_deleted'] += 1
logger.debug(f"删除文件: {bucket_name}/{obj.object_name}")
except Exception as e:
logger.error(f"删除文件失败 {bucket_name}/{obj.object_name}: {e}")
result['error'] = f"删除文件失败: {e}"
return result
# 删除空桶
self.rustfs.client.remove_bucket(bucket_name)
result['success'] = True
logger.info(f"桶 {bucket_name} 删除成功,共删除 {file_count} 个文件")
except Exception as e:
result['success'] = False
result['error'] = str(e)
logger.error(f"删除桶 {bucket_name} 失败: {e}")
return result
async def cleanup_all_old_buckets(self) -> Dict[str, Dict]:
"""清理所有旧桶"""
cleanup_results = {}
try:
# 检查所有桶状态
logger.info("=== 检查旧桶状态 ===")
bucket_status = await self.check_bucket_status()
# 统计需要清理的桶
buckets_to_clean = [
bucket_name for bucket_name, status in bucket_status.items()
if status['exists'] and status['file_count'] > 0
]
if not buckets_to_clean:
logger.info("没有需要清理的桶")
return {}
print(f"\n发现 {len(buckets_to_clean)} 个需要清理的桶:")
for bucket_name in buckets_to_clean:
status = bucket_status[bucket_name]
print(f" - {bucket_name}: {status['file_count']} 个文件, {status['total_size']} 字节")
# 确认清理
print("\n警告:此操作将永久删除这些桶及其所有文件!")
confirm = input("确认清理?(输入 'DELETE' 确认): ").strip()
if confirm != 'DELETE':
logger.info("用户取消清理操作")
return {}
# 执行清理
logger.info("=== 开始清理旧桶 ===")
for bucket_name in buckets_to_clean:
print(f"\n清理桶: {bucket_name}")
result = await self.delete_bucket(bucket_name)
cleanup_results[bucket_name] = result
if result['success']:
print(f" ✓ 清理成功,删除 {result['files_deleted']} 个文件")
else:
print(f" ✗ 清理失败: {result['error']}")
logger.info("=== 旧桶清理完成 ===")
except Exception as e:
logger.error(f"清理过程失败: {e}")
return cleanup_results
async def run_cleanup(self) -> Dict[str, any]:
"""运行完整清理流程"""
cleanup_summary = {
'start_time': datetime.now().isoformat(),
'connection_status': False,
'all_buckets': [],
'bucket_status': {},
'cleanup_results': {},
'end_time': None,
'status': 'failed'
}
try:
# 连接
logger.info("=== RustFS 旧桶清理开始 ===")
connection_result = await self.connect()
if not connection_result:
raise RuntimeError("无法连接到 RustFS")
cleanup_summary['connection_status'] = True
# 列出所有桶
all_buckets = await self.list_all_buckets()
cleanup_summary['all_buckets'] = all_buckets
# 检查状态
bucket_status = await self.check_bucket_status()
cleanup_summary['bucket_status'] = bucket_status
# 执行清理
cleanup_results = await self.cleanup_all_old_buckets()
cleanup_summary['cleanup_results'] = cleanup_results
# 完成
cleanup_summary['status'] = 'completed'
cleanup_summary['end_time'] = datetime.now().isoformat()
logger.info("=== RustFS 旧桶清理完成 ===")
return cleanup_summary
except Exception as e:
cleanup_summary['error'] = str(e)
cleanup_summary['end_time'] = datetime.now().isoformat()
logger.error(f"清理失败: {e}")
return cleanup_summary
finally:
await self.close()
async def main():
"""主函数"""
cleanup = RustFSCleanup()
print("=== RustFS 旧桶清理工具 ===")
print("注意:这是rustFS,而不是minio,只是用了minio的通用S3接口")
print("\n此工具将删除以下遗留桶:")
print(" - moldinsight-geometry")
print(" - moldinsight-stp-files")
print(" - moldinsight-mold-cavities")
print(" - moldinsight-html")
print(" - moldinsight-user-files")
print("\n请确保所有重要数据已备份!")
# 运行清理
result = await cleanup.run_cleanup()
# 输出结果
print("\n=== 清理结果摘要 ===")
print(f"状态: {result['status']}")
print(f"开始时间: {result['start_time']}")
print(f"结束时间: {result['end_time']}")
if 'error' in result:
print(f"错误: {result['error']}")
# 桶状态
print("\n--- 桶状态检查 ---")
for bucket_name, status in result['bucket_status'].items():
if status['exists']:
print(f"{bucket_name}: 存在, {status['file_count']} 个文件")
else:
print(f"{bucket_name}: 不存在")
# 清理结果
print("\n--- 清理结果 ---")
if result['cleanup_results']:
for bucket_name, cleanup_result in result['cleanup_results'].items():
if cleanup_result['success']:
print(f"{bucket_name}: 成功,删除 {cleanup_result['files_deleted']} 个文件")
else:
print(f"{bucket_name}: 失败 - {cleanup_result.get('error', '未知错误')}")
else:
print("未执行清理操作")
print("\n=== 清理完成 ===")
if __name__ == "__main__":
asyncio.run(main())
+2
View File
@@ -0,0 +1,2 @@
# config package
# 配置模块包
+92
View File
@@ -0,0 +1,92 @@
# config/settings.py
import os
import urllib.parse
from typing import Dict, Any
from dotenv import load_dotenv
# 加载.env文件
load_dotenv()
class Settings:
"""配置管理器"""
def __init__(self):
# 从环境变量加载配置
self.HOST = os.getenv('HOST', '0.0.0.0')
self.PORT = int(os.getenv('PORT', '8000'))
self.DEBUG = os.getenv('DEBUG', 'false').lower() == 'true'
# 文件处理配置
self.UPLOAD_DIR = os.getenv('UPLOAD_DIR', './uploads')
self.MAX_FILE_SIZE = int(os.getenv('MAX_FILE_SIZE', '104857600'))
self.ALLOWED_EXTENSIONS = os.getenv('ALLOWED_EXTENSIONS', '.stp,.step,.stp.gz')
# 几何处理配置
self.POINTCLOUD_SAMPLE_COUNT = int(os.getenv('POINTCLOUD_SAMPLE_COUNT', '10000'))
self.MESH_QUALITY = os.getenv('MESH_QUALITY', 'high')
self.PARALLEL_PROCESSING = os.getenv('PARALLEL_PROCESSING', 'true').lower() == 'true'
# RustFS 对象存储配置 (S3v4 API)
self.RUSTFS_ENDPOINT = os.getenv('RUSTFS_ENDPOINT', 'http://localhost:8080')
self.RUSTFS_ACCESS_KEY = os.getenv('RUSTFS_ACCESS_KEY', 'your-access-key')
self.RUSTFS_SECRET_KEY = os.getenv('RUSTFS_SECRET_KEY', 'your-secret-key')
self.RUSTFS_TIMEOUT = int(os.getenv('RUSTFS_TIMEOUT', '30'))
# 预签名URL过期时间(秒)
self.RUSTFS_PRESIGNED_URL_EXPIRES = int(os.getenv('RUSTFS_PRESIGNED_URL_EXPIRES', '3600'))
# 数据库配置 - 必须来自环境变量
# 先检查所有配置是否存在
db_host = os.getenv('DB_HOST')
db_port_str = os.getenv('DB_PORT')
db_name = os.getenv('DB_NAME')
db_user = os.getenv('DB_USER')
db_password = os.getenv('DB_PASSWORD')
missing_configs = []
if not db_host:
missing_configs.append("DB_HOST")
if not db_port_str:
missing_configs.append("DB_PORT")
if not db_name:
missing_configs.append("DB_NAME")
if not db_user:
missing_configs.append("DB_USER")
if not db_password:
missing_configs.append("DB_PASSWORD")
if missing_configs:
raise ValueError(f"数据库配置缺失,请在.env文件中设置: {', '.join(missing_configs)}")
# 所有配置都存在,进行赋值
self.DB_HOST = db_host
self.DB_PORT = int(db_port_str)
self.DB_NAME = db_name
self.DB_USER = db_user
self.DB_PASSWORD = db_password
# JWT配置
self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
self.ALGORITHM = os.getenv('ALGORITHM', 'HS256')
self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', '1440')) # 24小时
@property
def DATABASE_URL(self) -> str:
"""动态生成数据库连接URL"""
# 安全编码密码
if self.DB_PASSWORD:
safe_password = urllib.parse.quote(self.DB_PASSWORD.encode('utf-8'), safe='')
else:
safe_password = ""
return f"postgresql+asyncpg://{self.DB_USER}:{safe_password}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
@property
def allowed_extensions_set(self) -> set:
"""将ALLOWED_EXTENSIONS字符串转换为set"""
return set(ext.strip() for ext in self.ALLOWED_EXTENSIONS.split(','))
# 创建全局配置实例
settings = Settings()
+92
View File
@@ -0,0 +1,92 @@
# docker-compose.yml - 完整版(包含PostgreSQL和MinIO)
services:
postgres:
image: postgres:15
container_name: moldinsight_postgres
environment:
POSTGRES_DB: ${DB_NAME:-moldinsight}
POSTGRES_USER: ${DB_USER:-moldinsight_user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-moldinsight_user} -d ${DB_NAME:-moldinsight}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- moldinsight_network
minio:
image: minio/minio:latest
container_name: moldinsight_minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
ports:
- "9000:9000" # API端口
- "9001:9001" # 控制台端口
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
restart: unless-stopped
networks:
- moldinsight_network
moldinsight:
build: .
container_name: moldinsight_app
ports:
- "${HOST_PORT:-10001}:${CONTAINER_PORT:-8000}" # 宿主机端口:容器端口
volumes:
- ./uploads:/app/uploads
- ./html_output:/app/html_output
- ./logs:/app/logs
- ./.env:/app/.env:ro
environment:
# 应用端口配置(从settings.py读取)
- HOST=${HOST:-0.0.0.0}
- PORT=${CONTAINER_PORT:-8000}
# 数据库配置
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=${DB_NAME:-moldinsight}
- DB_USER=${DB_USER:-moldinsight_user}
- DB_PASSWORD=${DB_PASSWORD:-moldinsight_password}
# MinIO配置
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-minioadmin}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-minioadmin}
- MINIO_SECURE=false
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- moldinsight_network
volumes:
postgres_data:
driver: local
minio_data:
driver: local
networks:
moldinsight_network:
driver: bridge
+465
View File
@@ -0,0 +1,465 @@
# RustFS 对象存储集成说明
## 架构概述
本项目采用 **RustFS** 作为对象存储和 **PostgreSQL** 作为元数据存储的双层存储架构。
```
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (FastAPI) │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───────▼────────┐ ┌─────────▼─────────┐
│ PostgreSQL │ │ RustFS │
│ (元数据) │ │ (对象存储) │
│ │ │ │
│ - users │ │ - stp-files │
│ - stp_files │ │ - geometry │
│ - geometry_data│ │ - mold-cavities │
│ - mold_cavity │ │ - html-files │
│ - features │ │ - user-files │
│ - logs │ │ │
└────────────────┘ └──────────────────┘
```
## RustFS API 端点
假设 RustFS 运行在 `http://localhost:8080`,需要实现以下 REST API:
### 1. 健康检查
```
GET /health
返回: 200 OK
```
### 2. 初始化上传
```
POST /api/v1/upload/init
Content-Type: application/json
Authorization: Bearer <api_key>
请求体:
{
"namespace": "moldinsight/stp-files",
"key": "stp-files/abc123.stp",
"file_size": 1234567,
"file_hash": "sha256_hash",
"metadata": {
"original_filename": "model.stp",
"user_id": "1"
}
}
响应:
{
"upload_id": "unique-upload-id",
"upload_url": "https://rustfs/upload/xyz",
"expires_at": "2024-01-01T00:00:00Z"
}
```
### 3. 上传文件内容
```
PUT <upload_url>
Content-Type: application/octet-stream
Content-Length: 1234567
X-File-Hash: sha256_hash
请求体: <文件二进制数据>
响应: 201 Created
```
### 4. 完成上传
```
POST /api/v1/upload/complete
Content-Type: application/json
Authorization: Bearer <api_key>
请求体:
{
"upload_id": "unique-upload-id",
"namespace": "moldinsight/stp-files",
"key": "stp-files/abc123.stp"
}
响应:
{
"object_key": "moldinsight/stp-files/abc123.stp",
"etag": "d41d8cd98f00b204e9800998ecf8427e",
"created_at": "2024-01-01T00:00:00Z"
}
```
### 5. 上传 JSON(小文件,直接上传)
```
PUT /api/v1/objects/<namespace>/<key>
Content-Type: application/json
Authorization: Bearer <api_key>
X-File-Hash: sha256_hash
请求体: <JSON字符串>
响应: 201 Created
{
"object_key": "moldinsight/geometry/abc123.json",
"etag": "d41d8cd98f00b204e9800998ecf8427e",
"created_at": "2024-01-01T00:00:00Z"
}
```
### 6. 下载文件
```
GET /api/v1/objects/<namespace>/<key>
Authorization: Bearer <api_key>
响应: <文件二进制数据>
Content-Type: <原上传时的内容类型>
Content-Length: <文件大小>
ETag: <文件etag>
```
### 7. 获取文件信息
```
GET /api/v1/objects/<namespace>/<key>/info
Authorization: Bearer <api_key>
响应:
{
"object_key": "moldinsight/stp-files/abc123.stp",
"namespace": "moldinsight/stp-files",
"file_size": 1234567,
"file_hash": "sha256_hash",
"content_type": "application/octet-stream",
"created_at": "2024-01-01T00:00:00Z",
"last_modified": "2024-01-01T00:00:00Z",
"metadata": {
"original_filename": "model.stp",
"user_id": "1"
}
}
```
### 8. 删除文件
```
DELETE /api/v1/objects/<namespace>/<key>
Authorization: Bearer <api_key>
响应: 204 No Content
```
### 9. 列出文件
```
GET /api/v1/buckets/<namespace>/objects?prefix=stp-files
Authorization: Bearer <api_key>
响应:
{
"namespace": "moldinsight/stp-files",
"prefix": "stp-files",
"objects": [
{
"object_key": "moldinsight/stp-files/abc123.stp",
"file_size": 1234567,
"file_hash": "sha256_hash",
"created_at": "2024-01-01T00:00:00Z"
},
...
],
"is_truncated": false,
"next_marker": null
}
```
### 10. 生成预签名 URL
```
POST /api/v1/presigned-url
Content-Type: application/json
Authorization: Bearer <api_key>
请求体:
{
"namespace": "moldinsight/stp-files",
"key": "stp-files/abc123.stp",
"expires": 3600,
"method": "GET"
}
响应:
{
"url": "https://rustfs/objects/moldinsight/stp-files/abc123.stp?signature=xyz&expires=123",
"expires_at": "2024-01-01T01:00:00Z"
}
```
### 11. 存储统计
```
GET /api/v1/stats
Authorization: Bearer <api_key>
响应:
{
"total_objects": 1234,
"total_size": 1234567890,
"namespace_stats": {
"moldinsight/stp-files": {
"object_count": 100,
"total_size": 123456789
},
"moldinsight/geometry": {
"object_count": 200,
"total_size": 234567890
},
...
}
}
```
## 存储桶(命名空间)
| 命名空间 | 用途 | 存储内容 |
|---------|------|---------|
| `moldinsight/stp-files` | STP/STEP文件 | 用户上传的原始3D模型文件 |
| `moldinsight/geometry` | 几何数据 | 几何分析结果的JSON数据 |
| `moldinsight/mold-cavities` | 模具型腔数据 | 模具设计的详细JSON数据 |
| `moldinsight/html` | HTML文件 | 生成的HTML报告文件 |
| `moldinsight/user-files` | 用户文件 | 其他用户上传的文件 |
## 快速开始
### 1. 配置环境变量
```bash
# 复制示例配置
cp .env.example .env
# 编辑 .env 文件
nano .env
```
设置 RustFS 相关配置:
```env
RUSTFS_ENDPOINT=http://localhost:8080
RUSTFS_API_KEY=your-rustfs-api-key
RUSTFS_TIMEOUT=30
RUSTFS_PRESIGNED_URL_EXPIRES=3600
```
### 2. 启动 RustFS 服务
假设你已经有 RustFS 服务,如果没有,可以按照以下方式启动:
```bash
# 使用 Docker(如果提供了 Docker 镜像)
docker run -d \
--name rustfs \
-p 8080:8080 \
-e RUSTFS_API_KEY=your-rustfs-api-key \
-e RUSTFS_STORAGE_PATH=/data \
-v rustfs_data:/data \
your-registry/rustfs:latest
# 或直接运行编译好的二进制文件
./rustfs-server --port 8080 --api-key your-rustfs-api-key --storage-path ./rustfs-data
```
### 3. 初始化存储
```bash
# 初始化 RustFS 连接
python src/storage/init_storage.py
```
### 4. 使用存储集成服务
```python
from services.storage_integration_rustfs import storage_integration
from database.database import db_manager
async def upload_file(file_path: str):
async with db_manager.get_session() as session:
stp_file = await storage_integration.save_stp_file(
session=session,
file_path=Path(file_path),
original_filename="model.stp",
user_id=1
)
print(f"文件已保存到 RustFS,ID: {stp_file.id}")
```
## Python 客户端使用
### 上传文件
```python
from storage.rustfs_storage import rustfs_manager
from pathlib import Path
# 先连接
await rustfs_manager.connect(
endpoint="http://localhost:8080",
api_key="your-api-key"
)
# 上传 STP 文件
result = await rustfs_manager.upload_file(
bucket_type='stp_files',
file_path=Path('model.stp'),
original_filename='model.stp'
)
print(f"上传成功: {result['object_key']}")
```
### 上传 JSON 数据
```python
geometry_data = {
"volume": 5061079.99,
"surface_area": 640037.28,
"bounding_box": {...}
}
result = await rustfs_manager.upload_json_data(
bucket_type='geometry_data',
json_data=geometry_data,
file_hash='sha256-hash'
)
print(f"JSON 上传成功: {result['object_key']}")
```
### 下载文件
```python
data = await rustfs_manager.download_file(
bucket_type='geometry_data',
object_key='geometry_data/abc123.json'
)
json_data = json.loads(data.decode('utf-8'))
print(json_data)
```
### 生成预签名 URL
```python
url = await rustfs_manager.generate_presigned_url(
bucket_type='stp_files',
object_key='stp-files/abc123.stp',
expires=3600 # 1小时
)
print(f"临时访问链接: {url}")
```
### 列出文件
```python
files = await rustfs_manager.list_files(
bucket_type='stp_files',
prefix='stp-files'
)
for f in files:
print(f"{f['object_key']}: {f['file_size']} bytes")
```
## RustFS 服务端实现参考
如果你需要实现 RustFS 服务端,以下是一个简单的 Rust 实现框架:
```rust
use actix_web::{web, App, HttpServer};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Deserialize)]
struct UploadInitRequest {
namespace: String,
key: String,
file_size: u64,
file_hash: String,
metadata: Option<serde_json::Value>,
}
#[derive(Serialize)]
struct UploadInitResponse {
upload_id: String,
upload_url: String,
expires_at: String,
}
async fn upload_init(
req: web::Json<UploadInitRequest>,
path: web::Data<PathBuf>
) -> impl web::Responder {
// 验证 API Key
// 生成 upload_id
// 返回上传 URL
web::Json(UploadInitResponse { ... })
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health_check))
.route("/api/v1/upload/init", web::post().to(upload_init))
// ... 其他路由
})
.bind("0.0.0.0:8080")?
.run()
.await
}
```
## 故障排除
### 连接 RustFS 失败
```
错误: RustFS 连接失败
解决:
1. 检查 RUSTFS_ENDPOINT 是否正确
2. 检查 RustFS 服务是否运行
3. 检查网络连接
4. 验证 API Key
```
### 文件上传失败
```
错误: RustFS 上传失败
解决:
1. 检查磁盘空间
2. 检查网络连接
3. 检查文件大小限制
4. 查看服务端日志
```
### 预签名 URL 失败
```
错误: RustFS 生成预签名URL失败
解决:
1. 检查 expires 参数是否有效
2. 确认服务端支持预签名 URL
3. 检查权限配置
```
## 性能优化建议
### 客户端
- 使用连接池(aiohttp 默认支持)
- 实现上传重试机制
- 对大文件使用分片上传
- 并行上传多个小文件
### 服务端
- 实现缓存层
- 支持范围请求(断点续传)
- 压缩存储
- CDN 分发静态文件
## 安全建议
1. **更改默认 API Key**:生产环境必须使用强密钥
2. **启用 HTTPS**:生产环境使用 TLS
3. **访问控制**:配置命名空间权限
4. **数据加密**:敏感数据加密存储
5. **日志监控**:记录所有访问和操作
+255
View File
@@ -0,0 +1,255 @@
# 存储架构说明
## 架构概述
本项目采用 **MinIO (S3兼容)** 作为对象存储和 **PostgreSQL** 作为元数据存储的双层存储架构。
```
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (FastAPI) │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───────▼────────┐ ┌─────────▼─────────┐
│ PostgreSQL │ │ MinIO/S3 │
│ (元数据) │ │ (对象存储) │
│ │ │ │
│ - users │ │ - stp-files │
│ - stp_files │ │ - geometry │
│ - geometry_data│ │ - mold-cavities │
│ - mold_cavity │ │ - html-files │
│ - features │ │ - user-files │
│ - logs │ │ │
└────────────────┘ └──────────────────┘
```
## PostgreSQL 数据表
### 用户管理
- `users` - 用户信息(用户名、邮箱、密码等)
### 文件管理
- `stp_files` - STP文件元数据(文件名、哈希、大小、状态等)
- `html_files` - HTML报告文件元数据
### 几何数据
- `geometry_data` - 几何分析数据(体积、表面积、边界框等)
- `mold_cavity_data` - 模具型腔数据(工艺参数、质量评估等)
- `feature_detections` - 特征检测结果(壁厚、拔模角等)
- `design_recommendations` - 设计建议(优先级、参数等)
### 任务和日志
- `processing_tasks` - 处理任务记录
- `user_activities` - 用户活动日志
- `system_logs` - 系统日志
## MinIO 存储桶
| 存储桶名称 | 用途 | 存储内容 |
|-------------|------|---------|
| `moldinsight-stp-files` | STP/STEP文件 | 用户上传的原始3D模型文件 |
| `moldinsight-geometry` | 几何数据 | 几何分析结果的JSON数据 |
| `moldinsight-mold-cavities` | 模具型腔数据 | 模具设计的详细JSON数据 |
| `moldinsight-html` | HTML文件 | 生成的HTML报告文件 |
| `moldinsight-user-files` | 用户文件 | 其他用户上传的文件 |
## 快速开始
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 启动 PostgreSQL
使用 Docker Compose(推荐):
```bash
docker-compose up -d postgres
```
或手动启动:
```bash
# 创建数据库
createdb moldinsight
# 运行数据库容器
docker run -d \
--name postgres \
-e POSTGRES_DB=moldinsight \
-e POSTGRES_USER=moldinsight_user \
-e POSTGRES_PASSWORD=your_password \
-p 5432:5432 \
postgres:15
```
### 3. 启动 MinIO
使用 Docker Compose(推荐):
```bash
docker-compose up -d minio
```
或手动启动:
```bash
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"
```
### 4. 配置环境变量
```bash
# 复制示例配置
cp .env.example .env
# 编辑 .env 文件,修改数据库和MinIO配置
nano .env
```
### 5. 初始化数据库和存储
```bash
# 初始化数据库
python src/database/init_db.py
# 初始化MinIO存储
python src/storage/init_storage.py
```
### 6. 启动服务
```bash
python src/main.py
```
## 使用示例
### 保存STP文件
```python
from services.storage_integration import storage_integration
from database.database import db_manager
async def upload_file(file_path: str):
async with db_manager.get_session() as session:
stp_file = await storage_integration.save_stp_file(
session=session,
file_path=Path(file_path),
original_filename="model.stp",
user_id=1
)
print(f"文件已保存,ID: {stp_file.id}")
```
### 保存几何数据
```python
async def save_geometry(stp_file_id: int, geometry_data: dict):
async with db_manager.get_session() as session:
geo_data = await storage_integration.save_geometry_data(
session=session,
stp_file_id=stp_file_id,
geometry_json=geometry_data,
analysis_method="pythonocc"
)
print(f"几何数据已保存,ID: {geo_data.id}")
```
### 获取文件数据
```python
async def get_file_data(stp_file_id: int):
async with db_manager.get_session() as session:
data = await storage_integration.get_stp_file_with_data(
session=session,
stp_file_id=stp_file_id
)
# 访问几何数据
geometry = data['geometry_data']
print(f"体积: {geometry['volume']}")
print(f"表面积: {geometry['surface_area']}")
# 访问模具型腔数据
cavity = data['mold_cavity_data']
print(f"模具材料: {cavity['mold_material']}")
# 访问特征和建议
for feature in data['features']:
print(f"特征: {feature['feature_type']}")
```
## 数据清理策略
### MinIO 对象存储
- 设置生命周期策略自动删除旧文件
- 示例:删除30天前的临时文件
### PostgreSQL
- 定期清理已删除用户的记录
- 归档超过6个月的日志数据
## 监控和维护
### 检查存储使用情况
```bash
# MinIO控制台
# http://localhost:9001
# 用户名: minioadmin
# 密码: minioadmin
```
### 数据库备份
```bash
# 备份数据库
pg_dump -h localhost -U moldinsight_user moldinsight > backup.sql
# 恢复数据库
psql -h localhost -U moldinsight_user moldinsight < backup.sql
```
## 性能优化
### PostgreSQL
- 创建适当的索引(已在模型中定义)
- 定期运行 VACUUM 和 ANALYZE
- 考虑使用连接池(已配置)
### MinIO
- 启用缓存层
- 配置CDN分发静态文件
- 使用多区域复制
## 安全建议
1. **更改默认密码**:生产环境必须更改所有默认密码
2. **启用TLS**:生产环境启用 HTTPS
3. **访问控制**:配置适当的用户权限
4. **数据加密**:敏感数据加密存储
5. **定期备份**:设置自动备份策略
## 故障排除
### 连接MinIO失败
```
错误: 对象存储连接失败
解决: 检查 MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY 配置
```
### 数据库连接失败
```
错误: 数据库连接失败
解决: 检查 DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD 配置
```
### 文件上传失败
```
错误: STP文件上传失败
解决: 检查磁盘空间、网络连接、MinIO权限
```
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482413,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540528,
40.52568571347111,
-120.81246492337333
],
"inertia_properties": {
"mass": 5061079.998482413,
"moment_of_inertia": [
[
186102894991.01657,
-7899980049.787681,
-1579033714.9552822
],
[
-7899980049.787681,
30681971505.44673,
-1739615749.1562157
],
[
-1579033714.9552822,
-1739615749.1562157,
198160230180.50974
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482413,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540528,
40.52568571347111,
-120.81246492337333
],
"inertia_properties": {
"mass": 5061079.998482413,
"moment_of_inertia": [
[
186102894991.01657,
-7899980049.787681,
-1579033714.9552822
],
[
-7899980049.787681,
30681971505.44673,
-1739615749.1562157
],
[
-1579033714.9552822,
-1739615749.1562157,
198160230180.50974
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">5061080.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">640037.28 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">203.8 × 563.2 × 193.3 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">232</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">1337</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">2674</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
-102.15609222840467,
-266.68651824453497,
-200.2952105906499
],
"max": [
101.66140805628706,
296.56301479098704,
-6.96132576131294
],
"dimensions": [
203.81750028469173,
563.2495330355221,
193.33388482933697
],
"center": [
-0.2473420860588078,
14.938248273226037,
-103.62826817598142
]
},
"volume": 5061079.998482411,
"surface_area": 640037.2799399707,
"topology": {
"faces": 232,
"edges": 1337,
"vertices": 2674
},
"center_of_mass": [
-9.874164428540533,
40.525685713471205,
-120.81246492337347
],
"inertia_properties": {
"mass": 5061079.998482411,
"moment_of_inertia": [
[
186102894991.0164,
-7899980049.787681,
-1579033714.9552717
],
[
-7899980049.787681,
30681971505.446594,
-1739615749.1562958
],
[
-1579033714.9552717,
-1739615749.1562958,
198160230180.5097
]
]
},
"analysis_method": "pythonocc"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env python3
"""
RustFS 存储结构迁移脚本
将旧的多桶结构迁移到新的单桶结构:
旧结构: moldinsight-geometry, moldinsight-stp-files, moldinsight-mold-cavities, moldinsight-html-files, moldinsight-user-files
新结构: moldinsight-storage/
├── moldinsight/stp-files/{uuid}.stp
├── moldinsight/geometry/{file_hash}.json
├── moldinsight/mold-cavities/{file_hash}.json
├── moldinsight/html/{file_hash}.json
└── moldinsight/user-files/{uuid}.{ext}
注意:这是rustFS,而不是minio,只是用了minio的通用S3接口
"""
import asyncio
import sys
import os
from pathlib import Path
from typing import Dict, List, Optional
import json
from datetime import datetime
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent
src_root = project_root / "src"
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from storage.rustfs_storage import RustFSManager
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class RustFSMigration:
"""RustFS 存储结构迁移器"""
def __init__(self):
self.rustfs = RustFSManager()
self.is_connected = False
# 旧桶名称映射
self.old_buckets = {
'stp_files': 'moldinsight-stp-files',
'geometry_data': 'moldinsight-geometry',
'mold_cavities': 'moldinsight-mold-cavities',
'html_files': 'moldinsight-html-files',
'user_files': 'moldinsight-user-files'
}
# 新桶名称
self.new_bucket = 'moldinsight'
# 文件类型前缀映射
self.file_type_mapping = {
'stp_files': 'stp-files',
'geometry_data': 'geometry',
'mold_cavities': 'mold-cavities',
'html_files': 'html',
'user_files': 'user-files'
}
async def connect(self):
"""连接到 RustFS"""
try:
await self.rustfs.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
self.is_connected = True
logger.info("RustFS 连接成功")
return True
except Exception as e:
logger.error(f"RustFS 连接失败: {e}")
return False
async def close(self):
"""关闭连接"""
await self.rustfs.close()
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def list_old_buckets(self) -> Dict[str, List[Dict]]:
"""列出所有旧桶及其文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
buckets_info = {}
for file_type, bucket_name in self.old_buckets.items():
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(bucket_name):
logger.info(f"桶不存在: {bucket_name}")
buckets_info[bucket_name] = []
continue
# 列出桶中所有文件
objects = self.rustfs.client.list_objects(bucket_name, recursive=True)
files = []
for obj in objects:
files.append({
'object_key': obj.object_name,
'size': obj.size,
'last_modified': obj.last_modified,
'etag': obj.etag
})
buckets_info[bucket_name] = files
logger.info(f"桶 {bucket_name} 包含 {len(files)} 个文件")
except Exception as e:
logger.error(f"列出桶 {bucket_name} 失败: {e}")
buckets_info[bucket_name] = []
return buckets_info
async def ensure_new_bucket(self):
"""确保新桶存在"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
if not self.rustfs.client.bucket_exists(self.new_bucket):
self.rustfs.client.make_bucket(self.new_bucket)
logger.info(f"创建新桶: {self.new_bucket}")
else:
logger.info(f"新桶已存在: {self.new_bucket}")
except Exception as e:
logger.error(f"确保新桶存在失败: {e}")
raise
async def migrate_file(self, old_bucket: str, old_object_key: str, file_type: str) -> bool:
"""迁移单个文件到新结构"""
try:
# 下载旧文件
response = self.rustfs.client.get_object(old_bucket, old_object_key)
file_data = response.read()
response.close()
response.release_conn()
# 生成新对象键
if file_type in ['stp_files', 'user_files']:
# STP文件和用户文件:使用UUID格式
import uuid
unique_id = str(uuid.uuid4())
ext = Path(old_object_key).suffix or ('.stp' if file_type == 'stp_files' else '')
new_object_key = f"moldinsight/{self.file_type_mapping[file_type]}/{unique_id}{ext}"
else:
# JSON数据文件:使用文件哈希格式
import hashlib
file_hash = hashlib.sha256(file_data).hexdigest()
new_object_key = f"moldinsight/{self.file_type_mapping[file_type]}/{file_hash}.json"
# 上传到新桶
self.rustfs.client.put_object(
self.new_bucket,
new_object_key,
data=file_data,
length=len(file_data),
content_type='application/octet-stream'
)
logger.info(f"文件迁移成功: {old_bucket}/{old_object_key} -> {self.new_bucket}/{new_object_key}")
return True
except Exception as e:
logger.error(f"文件迁移失败 {old_bucket}/{old_object_key}: {e}")
return False
async def migrate_bucket(self, old_bucket: str, file_type: str) -> Dict[str, any]:
"""迁移整个桶"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
migration_result = {
'total_files': 0,
'successful': 0,
'failed': 0,
'failed_files': []
}
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(old_bucket):
logger.info(f"桶不存在,跳过迁移: {old_bucket}")
return migration_result
# 列出桶中所有文件
objects = self.rustfs.client.list_objects(old_bucket, recursive=True)
files = list(objects)
migration_result['total_files'] = len(files)
logger.info(f"开始迁移桶 {old_bucket}, 包含 {len(files)} 个文件")
for obj in files:
success = await self.migrate_file(old_bucket, obj.object_name, file_type)
if success:
migration_result['successful'] += 1
else:
migration_result['failed'] += 1
migration_result['failed_files'].append(obj.object_name)
logger.info(f"桶 {old_bucket} 迁移完成: 成功 {migration_result['successful']}, 失败 {migration_result['failed']}")
except Exception as e:
logger.error(f"桶 {old_bucket} 迁移失败: {e}")
return migration_result
async def delete_old_buckets(self) -> Dict[str, bool]:
"""删除所有旧桶(可选操作)"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
deletion_results = {}
for file_type, bucket_name in self.old_buckets.items():
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(bucket_name):
logger.info(f"桶不存在,跳过删除: {bucket_name}")
deletion_results[bucket_name] = True
continue
# 删除桶中所有文件
objects = self.rustfs.client.list_objects(bucket_name, recursive=True)
for obj in objects:
self.rustfs.client.remove_object(bucket_name, obj.object_name)
# 删除空桶
self.rustfs.client.remove_bucket(bucket_name)
deletion_results[bucket_name] = True
logger.info(f"桶删除成功: {bucket_name}")
except Exception as e:
deletion_results[bucket_name] = False
logger.error(f"桶删除失败 {bucket_name}: {e}")
return deletion_results
async def run_migration(self, delete_old_buckets: bool = False) -> Dict[str, any]:
"""运行完整迁移流程"""
migration_summary = {
'start_time': datetime.now().isoformat(),
'connection_status': False,
'old_buckets_info': {},
'migration_results': {},
'deletion_results': {},
'end_time': None,
'status': 'failed'
}
try:
# 1. 连接
logger.info("=== RustFS 存储结构迁移开始 ===")
connection_result = await self.connect()
if not connection_result:
raise RuntimeError("无法连接到 RustFS")
migration_summary['connection_status'] = True
# 2. 列出旧桶信息
logger.info("1. 检查旧桶结构...")
old_buckets_info = await self.list_old_buckets()
migration_summary['old_buckets_info'] = old_buckets_info
# 3. 确保新桶存在
logger.info("2. 确保新桶存在...")
await self.ensure_new_bucket()
# 4. 执行迁移
logger.info("3. 开始迁移文件...")
migration_results = {}
for file_type, bucket_name in self.old_buckets.items():
if old_buckets_info.get(bucket_name):
logger.info(f"迁移桶: {bucket_name}")
result = await self.migrate_bucket(bucket_name, file_type)
migration_results[bucket_name] = result
else:
logger.info(f"跳过空桶: {bucket_name}")
migration_summary['migration_results'] = migration_results
# 5. 可选:删除旧桶
if delete_old_buckets:
logger.info("4. 删除旧桶...")
deletion_results = await self.delete_old_buckets()
migration_summary['deletion_results'] = deletion_results
else:
logger.info("4. 保留旧桶(跳过删除)")
# 6. 完成
migration_summary['status'] = 'completed'
migration_summary['end_time'] = datetime.now().isoformat()
logger.info("=== RustFS 存储结构迁移完成 ===")
return migration_summary
except Exception as e:
migration_summary['error'] = str(e)
migration_summary['end_time'] = datetime.now().isoformat()
logger.error(f"迁移失败: {e}")
return migration_summary
finally:
await self.close()
async def main():
"""主函数"""
migration = RustFSMigration()
print("=== RustFS 存储结构迁移工具 ===")
print("注意:这是rustFS,而不是minio,只是用了minio的通用S3接口")
print()
# 询问是否删除旧桶
delete_old = input("是否在迁移完成后删除旧桶?(y/N): ").strip().lower() == 'y'
print("\n开始迁移...")
# 运行迁移
result = await migration.run_migration(delete_old_buckets=delete_old)
# 输出结果摘要
print("\n=== 迁移结果摘要 ===")
print(f"状态: {result['status']}")
print(f"开始时间: {result['start_time']}")
print(f"结束时间: {result['end_time']}")
if 'error' in result:
print(f"错误: {result['error']}")
# 旧桶信息
print("\n--- 旧桶信息 ---")
for bucket_name, files in result['old_buckets_info'].items():
print(f"{bucket_name}: {len(files)} 个文件")
# 迁移结果
print("\n--- 迁移结果 ---")
total_files = 0
total_success = 0
total_failed = 0
for bucket_name, migration_result in result['migration_results'].items():
print(f"{bucket_name}:")
print(f" 总文件数: {migration_result['total_files']}")
print(f" 成功: {migration_result['successful']}")
print(f" 失败: {migration_result['failed']}")
total_files += migration_result['total_files']
total_success += migration_result['successful']
total_failed += migration_result['failed']
print(f"\n总计: {total_files} 个文件, 成功 {total_success}, 失败 {total_failed}")
# 删除结果(如果执行了删除)
if result['deletion_results']:
print("\n--- 旧桶删除结果 ---")
for bucket_name, success in result['deletion_results'].items():
status = "成功" if success else "失败"
print(f"{bucket_name}: {status}")
print("\n=== 迁移完成 ===")
if __name__ == "__main__":
asyncio.run(main())
+457
View File
@@ -0,0 +1,457 @@
#!/usr/bin/env python3
"""
RustFS 存储结构迁移脚本 - 完整版
从旧结构迁移到新结构:
旧桶: moldinsight-storage
新桶: moldinsight/
├── stp-files/{uuid}.stp
├── geometry/{file_hash}.json
├── mold-cavities/{file_hash}.json
├── html/{file_hash}.json
└── user-files/{uuid}.{ext}
注意:这是rustFS,而不是minio,只是用了minio的通用S3接口
"""
import asyncio
import sys
import os
from pathlib import Path
from typing import Dict, List, Optional
import json
from datetime import datetime
import hashlib
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent
src_root = project_root / "src"
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from storage.rustfs_storage import RustFSManager
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class RustFSMigration:
"""RustFS 存储结构迁移器"""
def __init__(self):
self.rustfs = RustFSManager()
self.is_connected = False
# 旧桶名称
self.old_bucket = 'moldinsight-storage'
# 新桶名称
self.new_bucket = 'moldinsight'
# 文件类型前缀映射
self.file_type_mapping = {
'stp-files': 'stp_files',
'geometry': 'geometry_data',
'mold-cavities': 'mold_cavities',
'html': 'html_files',
'user-files': 'user_files'
}
async def connect(self):
"""连接到 RustFS"""
try:
await self.rustfs.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
self.is_connected = True
logger.info("RustFS 连接成功")
return True
except Exception as e:
logger.error(f"RustFS 连接失败: {e}")
return False
async def close(self):
"""关闭连接"""
await self.rustfs.close()
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def list_all_buckets(self) -> List[str]:
"""列出所有桶"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
buckets = self.rustfs.client.list_buckets()
bucket_names = [bucket.name for bucket in buckets]
logger.info(f"当前存在的桶: {bucket_names}")
return bucket_names
except Exception as e:
logger.error(f"列出桶失败: {e}")
return []
async def check_old_bucket_files(self) -> List[Dict]:
"""检查旧桶中的所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
# 检查旧桶是否存在
if not self.rustfs.client.bucket_exists(self.old_bucket):
logger.info(f"旧桶不存在: {self.old_bucket}")
return []
# 列出所有文件
objects = self.rustfs.client.list_objects(self.old_bucket, recursive=True)
files = []
for obj in objects:
files.append({
'object_key': obj.object_name,
'size': obj.size,
'last_modified': obj.last_modified,
'etag': obj.etag
})
logger.info(f"旧桶 {self.old_bucket} 包含 {len(files)} 个文件")
return files
except Exception as e:
logger.error(f"检查旧桶文件失败: {e}")
return []
async def ensure_new_bucket(self):
"""确保新桶存在"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
if not self.rustfs.client.bucket_exists(self.new_bucket):
self.rustfs.client.make_bucket(self.new_bucket)
logger.info(f"创建新桶: {self.new_bucket}")
else:
logger.info(f"新桶已存在: {self.new_bucket}")
except Exception as e:
logger.error(f"确保新桶存在失败: {e}")
raise
async def migrate_file(self, old_object_key: str) -> bool:
"""迁移单个文件到新结构"""
try:
# 下载旧文件
response = self.rustfs.client.get_object(self.old_bucket, old_object_key)
file_data = response.read()
response.close()
response.release_conn()
# 解析旧对象键,确定文件类型
# 旧格式: moldinsight/{文件类型}/{文件名} 或 {文件类型}/{文件名}
parts = old_object_key.split('/')
# 确定文件类型和新对象键
if len(parts) >= 2:
# 可能是 moldinsight/{类型}/{文件} 或 {类型}/{文件}
if parts[0] == 'moldinsight' and len(parts) >= 3:
# moldinsight/{类型}/{文件}
old_type = parts[1]
filename = parts[2]
elif parts[0] in self.file_type_mapping:
# {类型}/{文件}
old_type = parts[0]
filename = parts[1]
else:
# 无法识别的格式,使用默认
old_type = 'misc'
filename = parts[-1]
else:
old_type = 'misc'
filename = parts[-1]
# 根据旧类型确定新类型
new_type = old_type # 默认保持不变
# 生成新对象键
if old_type in self.file_type_mapping:
# 直接使用类型名作为目录
new_object_key = f"{old_type}/{filename}"
else:
# 其他文件放到misc目录
new_object_key = f"misc/{filename}"
# 上传到新桶
self.rustfs.client.put_object(
self.new_bucket,
new_object_key,
data=file_data,
length=len(file_data),
content_type='application/octet-stream'
)
logger.info(f"文件迁移成功: {self.old_bucket}/{old_object_key} -> {self.new_bucket}/{new_object_key}")
return True
except Exception as e:
logger.error(f"文件迁移失败 {old_object_key}: {e}")
return False
async def migrate_all_files(self) -> Dict[str, any]:
"""迁移所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
migration_result = {
'total_files': 0,
'successful': 0,
'failed': 0,
'failed_files': []
}
try:
# 获取所有文件
files = await self.check_old_bucket_files()
migration_result['total_files'] = len(files)
if len(files) == 0:
logger.info("旧桶中没有文件需要迁移")
return migration_result
logger.info(f"开始迁移 {len(files)} 个文件...")
for file_info in files:
old_object_key = file_info['object_key']
success = await self.migrate_file(old_object_key)
if success:
migration_result['successful'] += 1
else:
migration_result['failed'] += 1
migration_result['failed_files'].append(old_object_key)
logger.info(f"迁移完成: 成功 {migration_result['successful']}, 失败 {migration_result['failed']}")
except Exception as e:
logger.error(f"文件迁移失败: {e}")
return migration_result
async def delete_old_bucket(self) -> bool:
"""删除旧桶及其所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(self.old_bucket):
logger.info(f"旧桶不存在,无需删除: {self.old_bucket}")
return True
# 列出所有文件
objects = list(self.rustfs.client.list_objects(self.old_bucket, recursive=True))
if len(objects) > 0:
logger.info(f"删除旧桶中的 {len(objects)} 个文件...")
# 删除所有文件
for obj in objects:
self.rustfs.client.remove_object(self.old_bucket, obj.object_name)
logger.debug(f"删除文件: {obj.object_name}")
# 删除空桶
self.rustfs.client.remove_bucket(self.old_bucket)
logger.info(f"旧桶删除成功: {self.old_bucket}")
return True
except Exception as e:
logger.error(f"删除旧桶失败 {self.old_bucket}: {e}")
return False
async def list_new_bucket_structure(self) -> Dict[str, List[str]]:
"""列出新桶的文件结构"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
if not self.rustfs.client.bucket_exists(self.new_bucket):
return {}
objects = self.rustfs.client.list_objects(self.new_bucket, recursive=True)
structure = {
'stp-files': [],
'geometry': [],
'mold-cavities': [],
'html': [],
'user-files': [],
'misc': []
}
for obj in objects:
parts = obj.object_name.split('/')
if len(parts) >= 2:
file_type = parts[0]
if file_type in structure:
structure[file_type].append(obj.object_name)
else:
structure['misc'].append(obj.object_name)
return structure
except Exception as e:
logger.error(f"列出新桶结构失败: {e}")
return {}
async def run_migration(self, delete_old_bucket: bool = False) -> Dict[str, any]:
"""运行完整迁移流程"""
migration_summary = {
'start_time': datetime.now().isoformat(),
'connection_status': False,
'all_buckets': [],
'old_bucket_files': [],
'migration_results': {},
'new_bucket_structure': {},
'deletion_result': False,
'end_time': None,
'status': 'failed'
}
try:
# 1. 连接
logger.info("=== RustFS 存储结构迁移开始 ===")
connection_result = await self.connect()
if not connection_result:
raise RuntimeError("无法连接到 RustFS")
migration_summary['connection_status'] = True
# 2. 列出所有桶
logger.info("1. 检查桶状态...")
all_buckets = await self.list_all_buckets()
migration_summary['all_buckets'] = all_buckets
# 3. 检查旧桶文件
logger.info("2. 检查旧桶文件...")
old_bucket_files = await self.check_old_bucket_files()
migration_summary['old_bucket_files'] = old_bucket_files
if not old_bucket_files:
logger.info("旧桶中没有文件,跳过迁移")
migration_summary['status'] = 'completed'
migration_summary['end_time'] = datetime.now().isoformat()
return migration_summary
# 4. 确保新桶存在
logger.info("3. 确保新桶存在...")
await self.ensure_new_bucket()
# 5. 执行迁移
logger.info("4. 开始迁移文件...")
migration_results = await self.migrate_all_files()
migration_summary['migration_results'] = migration_results
# 6. 检查新桶结构
logger.info("5. 检查新桶结构...")
new_bucket_structure = await self.list_new_bucket_structure()
migration_summary['new_bucket_structure'] = new_bucket_structure
# 7. 可选:删除旧桶
if delete_old_bucket:
logger.info("6. 删除旧桶...")
deletion_result = await self.delete_old_bucket()
migration_summary['deletion_result'] = deletion_result
else:
logger.info("6. 保留旧桶(跳过删除)")
# 8. 完成
migration_summary['status'] = 'completed'
migration_summary['end_time'] = datetime.now().isoformat()
logger.info("=== RustFS 存储结构迁移完成 ===")
return migration_summary
except Exception as e:
migration_summary['error'] = str(e)
migration_summary['end_time'] = datetime.now().isoformat()
logger.error(f"迁移失败: {e}")
return migration_summary
finally:
await self.close()
async def main():
"""主函数"""
migration = RustFSMigration()
print("=== RustFS 存储结构迁移工具 ===")
print("注意:这是rustFS,而不是minio,只是用了minio的通用S3接口")
print()
print("从旧结构迁移到新结构:")
print(" 旧桶: moldinsight-storage")
print(" 新桶: moldinsight/")
print(" ├── stp-files/")
print(" ├── geometry/")
print(" ├── mold-cavities/")
print(" ├── html/")
print(" └── user-files/")
print()
# 询问是否删除旧桶
delete_old = input("是否在迁移完成后删除旧桶 moldinsight-storage?(y/N): ").strip().lower() == 'y'
print("\n开始迁移...")
# 运行迁移
result = await migration.run_migration(delete_old_bucket=delete_old)
# 输出结果摘要
print("\n=== 迁移结果摘要 ===")
print(f"状态: {result['status']}")
print(f"开始时间: {result['start_time']}")
print(f"结束时间: {result['end_time']}")
if 'error' in result:
print(f"错误: {result['error']}")
# 桶列表
print("\n--- 当前桶列表 ---")
for bucket in result['all_buckets']:
print(f" - {bucket}")
# 旧桶文件统计
print(f"\n--- 旧桶文件统计 ---")
print(f"旧桶 {migration.old_bucket} 包含 {len(result['old_bucket_files'])} 个文件")
# 迁移结果
print("\n--- 迁移结果 ---")
migration_result = result['migration_results']
print(f"总文件数: {migration_result['total_files']}")
print(f"成功: {migration_result['successful']}")
print(f"失败: {migration_result['failed']}")
if migration_result['failed'] > 0:
print("\n失败的文件:")
for failed_file in migration_result['failed_files']:
print(f" - {failed_file}")
# 新桶结构
print("\n--- 新桶文件结构 ---")
for file_type, files in result['new_bucket_structure'].items():
if files:
print(f"{file_type}: {len(files)} 个文件")
# 删除结果
if 'deletion_result' in result:
status = "成功" if result['deletion_result'] else "失败"
print(f"\n--- 旧桶删除结果 ---")
print(f"旧桶 {migration.old_bucket} 删除: {status}")
print("\n=== 迁移完成 ===")
if __name__ == "__main__":
asyncio.run(main())
+40
View File
@@ -0,0 +1,40 @@
# requirements.txt
# 几何处理核心
-i https://mirrors.aliyun.com/pypi/simple/
--trusted-host mirrors.aliyun.com
# pythonocc-core==7.7.0
pyvista
trimesh
numpy
scipy
dotenv
fastapi
numpy
OCC
# Web框架
fastapi
uvicorn
pydantic
python-multipart
# 数据库
sqlalchemy
psycopg2-binary
asyncpg
# 对象存储 (RustFS S3v4)
minio
aiohttp
# 消息队列
kafka-python
redis
# 工具库
aiofiles
python-dotenv
jinja2
# JWT认证
python-jose[cryptography]
passlib[bcrypt]
+1
View File
@@ -0,0 +1 @@
# API 模块
+327
View File
@@ -0,0 +1,327 @@
# api/routes.py
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Request, Depends
from typing import Optional
import uuid
from datetime import datetime
from pathlib import Path
from models.schemas import ProcessingStatus, create_task_info
from core.stp_parser import STPParser
from core.geometry_analyzer import GeometryAnalyzer
from utils.file_handler import FileHandler
from utils.html_generator import HTMLGenerator
from services.storage_integration_rustfs import StorageIntegrationService
from database.database import get_db_session
from utils.logger import get_logger
from sqlalchemy.ext.asyncio import AsyncSession
from core.mold_generator import MoldCavityGenerator
logger = get_logger(__name__)
router = APIRouter()
# 服务实例
stp_parser = STPParser()
geometry_analyzer = GeometryAnalyzer()
file_handler = FileHandler()
html_generator = HTMLGenerator()
# 初始化模具生成器(可配置不同材料的收缩率)
mold_generator = MoldCavityGenerator(shrinkage_rate=0.005) # ABS材料
# 内存中的任务存储
tasks = {}
@router.get("/")
async def read_root(request: Request):
"""主页面"""
from fastapi.templating import Jinja2Templates
import os
# 使用绝对路径确保模板目录正确
templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "templates")
templates = Jinja2Templates(directory=templates_dir)
return templates.TemplateResponse("index.html", {
"request": request,
"pythonocc_available": True,
"version": "3.0.0"
})
@router.get("/health")
async def health():
return {
"status": "healthy",
"pythonocc": True,
"total_tasks": len(tasks)
}
@router.post("/upload")
async def upload_stp(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
db_session: AsyncSession = Depends(get_db_session)
):
"""上传STP文件并存储到数据库"""
if not file.filename.lower().endswith(('.stp', '.step')):
raise HTTPException(400, "只支持STP/STEP文件")
task_id = str(uuid.uuid4())
# 保存文件
file_path = await file_handler.save_uploaded_file(file)
content = await file.read()
# 创建存储集成服务实例
storage_service = StorageIntegrationService()
# 保存STP文件到RustFS + PostgreSQL
stp_file = await storage_service.save_stp_file(
session=db_session,
file_path=file_path,
original_filename=file.filename
)
# 创建处理任务记录
await storage_service.create_processing_task(db_session, task_id, stp_file.id)
# 创建内存任务记录
tasks[task_id] = create_task_info(
task_id=task_id,
status=ProcessingStatus.PROCESSING,
filename=file.filename,
file_path=str(file_path),
file_size=len(content),
upload_time=str(datetime.now())
)
# 后台处理(包含数据库存储)
background_tasks.add_task(process_file_with_storage, task_id, file_path, stp_file.id, db_session)
return {
"task_id": task_id,
"status": "processing",
"message": "文件上传成功,开始处理并存储到数据库",
"file_info": {
"filename": file.filename,
"size": len(content),
"pythonocc_available": True,
"database_file_id": stp_file.id
}
}
@router.get("/status/{task_id}")
async def get_status(task_id: str):
"""获取任务状态"""
if task_id not in tasks:
raise HTTPException(404, "任务不存在")
task = tasks[task_id]
logger.info(f"返回任务状态: {task_id} - {task['status']}")
return task
@router.get("/debug/tasks")
async def debug_tasks():
"""调试接口:查看所有任务"""
return {
"total_tasks": len(tasks),
"tasks": tasks
}
async def process_file_with_storage(
task_id: str,
file_path: str,
stp_file_id: int,
db_session: AsyncSession
):
"""处理文件的后台任务"""
storage_service = StorageIntegrationService()
try:
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
# 设置处理超时(5分钟)
import asyncio
timeout_seconds = 300 # 5分钟
async def process_with_timeout():
# 处理逻辑将在下面添加
pass
# 使用超时保护
try:
await asyncio.wait_for(process_file_core(storage_service, task_id, file_path, stp_file_id, db_session), timeout_seconds)
except asyncio.TimeoutError:
logger.error(f"处理超时: {task_id}")
raise Exception(f"处理超时,超过{timeout_seconds}秒未完成")
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
await storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await storage_service.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
tasks[task_id]["status"] = ProcessingStatus.FAILED
tasks[task_id]["error"] = str(e)
tasks[task_id]["completed_at"] = str(datetime.now())
return
async def process_file_core(
storage_service: StorageIntegrationService,
task_id: str,
file_path: str,
stp_file_id: int,
db_session: AsyncSession
):
"""核心处理逻辑"""
try:
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
# 更新任务状态
await storage_service.update_task_status(
db_session, task_id, "processing", 20, "解析STP文件"
)
# 1. 解析STP文件
await storage_service.update_task_status(
db_session, task_id, "processing", 20, "解析STP文件"
)
# 使用STPParser类进行真实解析
shape = stp_parser.load_step_file(Path(file_path))
geometry_data = stp_parser.analyze_geometry(shape)
# 2. 生成模具型腔(模拟数据)
await storage_service.update_task_status(
db_session, task_id, "processing", 40, "生成模具型腔(模拟)"
)
cavity_data = {
"cavity_count": 1,
"cavity_dimensions": {"length": 100, "width": 80, "height": 50},
"runner_system": "cold_runner",
"gating_type": "edge_gate"
}
# 3. 生成详细JSON数据(模拟)
await storage_service.update_task_status(
db_session, task_id, "processing", 60, "生成型腔详细数据(模拟)"
)
detailed_cavity_json = {
"metadata": {
"file_name": Path(file_path).name,
"analysis_date": datetime.now().isoformat(),
"shrinkage_rate": 0.005,
"draft_angle": 2.0
},
"product_analysis": {
"volume": geometry_data["volume"],
"surface_area": geometry_data["surface_area"],
"bounding_box": geometry_data["bounding_box"]
},
"manufacturing_info": {
"recommended_material": "ABS",
"estimated_clamping_force": "150 吨",
"estimated_mold_size": {
"length": 120,
"width": 100,
"height": 60
}
},
"mold_cavities": {
"cavity_count": 1,
"cavity_key_info": {
"geometric_characteristics": {
"product_weight": "1.2 g",
"wall_thickness_range": "1.5-3.0 mm",
"complexity_score": 0.7
},
"quality_considerations": {
"potential_weld_lines": "center",
"sink_mark_areas": "thick_sections",
"warpage_risk": "low"
}
}
}
}
# 4. 生成关键信息(模拟)
cavity_key_info = detailed_cavity_json["mold_cavities"]["cavity_key_info"]
# 5. 保存几何数据到数据库
await storage_service.update_task_status(
db_session, task_id, "processing", 70, "保存几何数据"
)
geometry_record = await storage_service.save_geometry_data(
db_session,
stp_file_id,
geometry_data,
geometry_data.get("analysis_method", "mold_cavity")
)
# 6. 保存模具型腔数据
await storage_service.save_mold_cavity_data(
db_session,
stp_file_id,
detailed_cavity_json
)
# 7. 生成HTML可视化(包含型腔信息)
await storage_service.update_task_status(
db_session, task_id, "processing", 85, "生成可视化报告"
)
html_file_path = html_generator.generate_and_save_visualization(
geometry_data,
Path(file_path).name
)
# 保存HTML文件信息
html_record = await storage_service.save_html_file(
db_session,
stp_file_id,
Path(html_file_path).name,
html_file_path
)
# 8. 分析模具设计
analysis_result = geometry_analyzer.analyze_mold_design(geometry_data)
# 9. 完成处理
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
await storage_service.update_task_status(
db_session, task_id, "completed", 100, "模具型腔生成完成"
)
# 更新内存任务状态
tasks[task_id]["geometry_data"] = geometry_data
tasks[task_id]["analysis_result"] = analysis_result
tasks[task_id]["cavity_data"] = detailed_cavity_json
tasks[task_id]["key_info"] = cavity_key_info
tasks[task_id]["status"] = ProcessingStatus.COMPLETED
tasks[task_id]["completed_at"] = str(datetime.now())
logger.info(f"模具型腔生成完成: {task_id}")
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
await storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await storage_service.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
tasks[task_id]["status"] = ProcessingStatus.FAILED
tasks[task_id]["error"] = str(e)
tasks[task_id]["completed_at"] = str(datetime.now())
+1
View File
@@ -0,0 +1 @@
# Core 模块
+340
View File
@@ -0,0 +1,340 @@
# core/geometry_analyzer.py
from typing import Dict, List, Any
# import features # 暂时注释掉,避免导入错误
import numpy as np
from models.schemas import (
create_mold_feature,
create_design_recommendation,
create_analysis_result
)
from utils.logger import get_logger
logger = get_logger(__name__)
class GeometryAnalyzer:
"""几何分析器 - 简化版"""
def __init__(self):
self.feature_thresholds = {
"thin_wall": 2.0,
"thick_wall": 8.0,
"small_feature": 5.0,
"large_feature": 1000.0,
"high_complexity": 50,
}
self.product_materials = {
"ABS": {"shrinkage": 0.005, "min_wall": 1.2},
"PP": {"shrinkage": 0.016, "min_wall": 1.0},
"PC": {"shrinkage": 0.007, "min_wall": 1.5},
}
self.mold_materials = {
"Aluminum": {"thermal_conductivity": 200, "hardness": "HB80", "cost": "low"},
"P20_Steel": {"thermal_conductivity": 30, "hardness": "HRC30", "cost": "medium"},
"H13_Steel": {"thermal_conductivity": 25, "hardness": "HRC48", "cost": "high"}
}
def analyze_mold_design(self, geometry_data: Dict[str, Any],
product_material: str = "ABS",
mold_material: str = "Aluminum"
) -> Dict[str, Any]:
"""分析模具设计"""
logger.info("开始模具设计分析")
# 检测特征
features = self._detect_features(geometry_data)
# 使用产品材料属性
product_props = self.product_materials.get(product_material, {})
shrinkage = product_props.get("shrinkage", 0.005)
# 使用模具材料属性
mold_props = self.mold_materials.get(mold_material, {})
thermal_cond = mold_props.get("thermal_conductivity", 200)
# 生成设计建议
recommendations = self._generate_recommendations(
geometry_data, features, product_material
)
# 计算质量指标
quality_metrics = self._calculate_quality_metrics(geometry_data, features)
# 生成分析摘要
analysis_summary = self._generate_analysis_summary(geometry_data, features, recommendations)
return create_analysis_result(
geometry_data=geometry_data,
detected_features=features,
design_recommendations=recommendations,
quality_metrics=quality_metrics,
analysis_summary=analysis_summary
)
def _detect_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""检测模具特征"""
features = []
# 壁厚分析
wall_features = self._detect_wall_features(geometry_data)
features.extend(wall_features)
# 加强筋检测
rib_features = self._detect_rib_features(geometry_data)
features.extend(rib_features)
# BOSS柱检测
boss_features = self._detect_boss_features(geometry_data)
features.extend(boss_features)
# 拔模角度分析
draft_features = self._analyze_draft_angles(geometry_data)
features.extend(draft_features)
logger.info(f"检测到 {len(features)} 个特征")
return features
def _detect_wall_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""检测壁厚特征"""
features = []
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
if avg_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.85,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"考虑增加加强筋以提高结构强度",
"检查注塑填充是否充分"
]
))
elif avg_thickness > self.feature_thresholds["thick_wall"]:
features.append(create_mold_feature(
feature_type="thick_wall",
confidence=0.75,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
"优化冷却系统设计"
]
))
elif volume > 0:
# 如果没有surface_area,基于边界框估算壁厚
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
if bbox_volume > 0:
volume_efficiency = volume / bbox_volume
avg_thickness = (dimensions[0] + dimensions[1]) / 2 * volume_efficiency
if avg_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.7,
location=bbox.get("center", [50, 50, 50]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness, "estimation_method": "bbox_based"},
recommendations=[
f"估算平均壁厚 {avg_thickness:.2f}mm 过薄,建议检查表面积数据",
"考虑增加加强筋以提高结构强度"
]
))
return features
def _detect_rib_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""检测加强筋特征"""
features = []
topology = geometry_data.get("topology", {})
face_count = topology.get("faces", 0)
edge_count = topology.get("edges", 0)
complexity_ratio = edge_count / max(face_count, 1)
if complexity_ratio > 3.0:
features.append(create_mold_feature(
feature_type="rib_structure",
confidence=0.7,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[2.0, 8.0, 2.0],
parameters={"complexity_ratio": complexity_ratio},
recommendations=[
"检测到可能的加强筋结构",
"建议加强筋厚度为壁厚的50-80%",
"加强筋高度不超过壁厚的3倍",
"加强筋根部增加圆角避免应力集中"
]
))
return features
def _detect_boss_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""检测BOSS柱特征"""
features = []
volume = geometry_data.get("volume", 0)
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume_efficiency = volume / (dimensions[0] * dimensions[1] * dimensions[2])
if volume_efficiency < 0.3:
features.append(create_mold_feature(
feature_type="boss_feature",
confidence=0.65,
location=bbox.get("center", [50, 50, 50]),
dimensions=[6.0, 12.0, 6.0],
parameters={"volume_efficiency": volume_efficiency},
recommendations=[
"检测到可能的BOSS柱结构",
"建议BOSS柱外径为螺钉直径的2-2.5倍",
"BOSS柱高度不超过直径的2倍",
"增加拔模角度1-2度",
"根部增加圆角R0.5-R1.0"
]
))
return features
def _analyze_draft_angles(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""分析拔模角度"""
features = []
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.8,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[1.0, 2.0, 1.0],
parameters={"recommended_angle": 2.0},
recommendations=[
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
"深腔结构需要更大的拔模角度"
]
))
return features
def _generate_recommendations(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]],
material: str) -> List[Dict[str, Any]]:
"""生成设计建议"""
recommendations = []
# 壁厚建议
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material)
if wall_rec:
recommendations.append(wall_rec)
# 拔模角度建议
recommendations.append(create_design_recommendation(
rec_type="draft_angle",
priority="high",
description="添加拔模角度",
parameters={"min_angle": 1.0, "preferred_angle": 2.0},
reason="确保顺利脱模"
))
# 基于检测到的特征生成建议
for feature in features:
if feature["feature_type"] == "thin_wall":
rec = create_design_recommendation(
rec_type="wall_thickness",
priority="high",
description="增加壁厚",
parameters={
"current": feature["parameters"]["average_thickness"],
"recommended": self.feature_thresholds["thin_wall"]
},
reason="壁厚不足影响结构强度"
)
recommendations.append(rec)
return recommendations
def _get_wall_thickness_recommendation(self, geometry_data: Dict[str, Any],
material: str) -> Dict[str, Any]:
"""获取壁厚建议"""
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
material_props = self.product_materials.get(material, self.product_materials["ABS"])
min_wall = material_props["min_wall"]
if avg_thickness < min_wall:
return create_design_recommendation(
rec_type="wall_thickness",
priority="high",
description=f"增加壁厚至{min_wall}mm以上",
parameters={"current": avg_thickness, "recommended": min_wall},
reason=f"{material}材料最小壁厚要求"
)
return None
def _calculate_quality_metrics(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]]) -> Dict[str, float]:
"""计算质量指标"""
metrics = {}
# 体积利用率
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume = geometry_data.get("volume", 0)
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
metrics["volume_utilization"] = volume / bbox_volume if bbox_volume > 0 else 0
# 拓扑复杂度
topology = geometry_data.get("topology", {})
face_count = topology.get("faces", 0)
metrics["topology_complexity"] = face_count / 100.0
# 壁厚均匀性评分
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
thickness_ratio = (volume / surface_area) * 0.6
ideal_thickness = 3.0
metrics["wall_uniformity"] = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
elif volume > 0 and bbox_volume > 0:
# 如果没有surface_area,基于体积利用率估算
metrics["wall_uniformity"] = max(0.5, metrics["volume_utilization"])
else:
metrics["wall_uniformity"] = 0.5
return metrics
def _generate_analysis_summary(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]],
recommendations: List[Dict[str, Any]]) -> str:
"""生成分析摘要"""
volume = geometry_data.get("volume", 0)
high_priority_recs = len([r for r in recommendations if r["priority"] == "high"])
summary_parts = []
if volume > 0:
summary_parts.append(f"模型体积: {volume / 1000:.1f} cm³")
if features:
feature_types = set(f["feature_type"] for f in features)
summary_parts.append(f"检测到 {len(feature_types)} 类特征")
if high_priority_recs > 0:
summary_parts.append(f"有 {high_priority_recs} 个高优先级建议")
return " | ".join(summary_parts) if summary_parts else "分析完成"
+102
View File
@@ -0,0 +1,102 @@
# src/core/mesh_generator.py
import logging
import numpy as np
from typing import Dict, List, Optional
import pyvista as pv
import trimesh
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
logger = logging.getLogger(__name__)
class MeshGenerator:
"""网格生成器 - 使用PyVista和Trimesh"""
def __init__(self, quality: str = "medium"):
self.quality_settings = {
"low": 0.5,
"medium": 0.1,
"high": 0.01
}
self.quality = self.quality_settings.get(quality, 0.1)
def generate_mesh_from_shape(self, shape, num_points: int = 10000) -> Dict:
"""从形状生成网格数据"""
try:
# 方法1: 使用PythonOCC生成网格
occ_mesh = self._generate_occ_mesh(shape)
# 方法2: 转换为PyVista网格
pv_mesh = self._convert_to_pyvista(occ_mesh)
# 方法3: 转换为Trimesh网格
tri_mesh = self._convert_to_trimesh(pv_mesh)
# 生成点云
pointcloud = self._generate_pointcloud(tri_mesh, num_points)
return {
"pyvista_mesh": pv_mesh,
"trimesh_mesh": tri_mesh,
"pointcloud": pointcloud
}
except Exception as e:
logger.error(f"网格生成失败: {e}")
raise
def _generate_occ_mesh(self, shape) -> any:
"""使用PythonOCC生成网格"""
mesh = BRepMesh_IncrementalMesh(shape, self.quality)
mesh.Perform()
return mesh
def _convert_to_pyvista(self, occ_mesh) -> pv.PolyData:
"""转换为PyVista网格"""
# 这里需要从OCC网格中提取顶点和面数据
# 简化实现 - 实际需要遍历OCC网格数据结构
try:
# 创建示例网格数据
cube = pv.Cube()
return cube
except Exception as e:
logger.warning(f"PyVista转换失败,使用备用方法: {e}")
return self._create_sample_mesh()
def _convert_to_trimesh(self, pv_mesh) -> trimesh.Trimesh:
"""转换为Trimesh网格"""
try:
# 从PyVista转换
vertices = pv_mesh.points
faces = pv_mesh.faces.reshape(-1, 4)[:, 1:4] # 假设三角形网格
return trimesh.Trimesh(vertices=vertices, faces=faces)
except Exception as e:
logger.warning(f"Trimesh转换失败: {e}")
return self._create_sample_trimesh()
def _generate_pointcloud(self, mesh: trimesh.Trimesh, num_points: int) -> Dict:
"""从网格生成点云"""
try:
# 均匀采样点云
points, face_indices = trimesh.sample.sample_surface(mesh, num_points)
# 计算法向量
normals = mesh.face_normals[face_indices]
return {
"points": points.tolist(),
"normals": normals.tolist(),
"count": len(points)
}
except Exception as e:
logger.error(f"点云生成失败: {e}")
raise
def _create_sample_mesh(self) -> pv.PolyData:
"""创建示例网格(备用)"""
return pv.Cube()
def _create_sample_trimesh(self) -> trimesh.Trimesh:
"""创建示例Trimesh(备用)"""
return trimesh.creation.box([100, 80, 50])
+523
View File
@@ -0,0 +1,523 @@
# src/core/mold_generator.py
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.Geom import Geom_Plane
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger
logger = get_logger(__name__)
class MoldCavityGenerator:
"""模具型腔生成器 - 基于产品模型生成Cavity和Core"""
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0):
"""
初始化模具生成器
Args:
shrinkage_rate: 收缩率(默认0.5% for ABS)
draft_angle: 拔模角(默认2度)
"""
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle # 度
# 分型面检测参数
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
def generate_mold_cavities(self, product_shape: Any) -> Dict[str, Any]:
"""
从产品的3D模型生成型腔和型芯
Returns:
{
"cavity": cavity_shape, # 型腔(产品外部)
"core": core_shape, # 型芯(产品内部)
"parting_surface": parting_surface, # 分型面
"parting_line": parting_line # 分型线
}
"""
logger.info("开始生成模具型腔...")
try:
# Step 1: 分析产品几何
analysis = self._analyze_product_geometry(product_shape)
# Step 2: 检测分型面和分型线
parting_surface, parting_line = self._detect_parting_surface(
product_shape, analysis
)
# Step 3: 应用收缩率补偿
scaled_shape = self._apply_shrinkage_compensation(product_shape)
# Step 4: 添加拔模角
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
# Step 5: 分离型腔和型芯
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
logger.info("模具型腔生成完成")
return {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
"parting_line": parting_line,
"analysis": analysis
}
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
raise
def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]:
"""
生成详细的型腔三维JSON数据
Returns:
包含完整几何信息的JSON结构
"""
cavity = cavity_data["cavity"]
core = cavity_data["core"]
parting_surface = cavity_data["parting_surface"]
analysis = cavity_data["analysis"]
# 提取型腔几何数据
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
core_geometry = self._extract_shape_geometry(core, "core")
# 提取分型面数据
parting_geometry = self._extract_parting_surface_geometry(
parting_surface
)
detailed_json = {
"metadata": {
"version": "2.0",
"generated_at": str(np.datetime64('now')),
"shrinkage_rate": self.shrinkage_rate,
"draft_angle": self.draft_angle,
"unit": "mm"
},
"product_analysis": {
"bounding_box": analysis.get("bounding_box", {}), # 使用get方法
"volume": analysis.get("volume", 0), # 使用get方法
"surface_area": analysis.get("surface_area", 0), # 使用get方法
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0]) # 使用get方法
},
"mold_cavities": {
"cavity": cavity_geometry,
"core": core_geometry
},
"parting_surface": parting_geometry,
"manufacturing_info": {
"estimated_mold_size": self._calculate_mold_size(analysis),
"estimated_clamping_force": self._calculate_clamping_force(analysis),
"recommended_material": self._get_recommended_material()
}
}
return detailed_json
def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]:
"""
生成模具型腔的关键信息
Returns:
关键参数摘要
"""
analysis = cavity_data["analysis"]
key_info = {
"mold_parameters": {
"shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%",
"draft_angle": f"{self.draft_angle}°",
"parting_line_length": self._calculate_parting_line_length(
cavity_data["parting_line"]
),
"cavity_depth": analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])[2]
},
"geometric_characteristics": {
"product_volume": f"{analysis.get('volume', 0) / 1000:.2f} cm³",
"product_weight": self._calculate_product_weight(analysis),
"wall_thickness_range": self._estimate_wall_thickness(analysis),
"complexity_score": self._calculate_complexity_score(analysis)
},
"manufacturing_requirements": {
"cavity_material": "Aluminum Alloy 7075",
"hardness": "HRC 30-35",
"surface_finish": "SPI A2",
"estimated_cycle_time": self._estimate_cycle_time(analysis),
"recommended_injection_pressure": "80-120 MPa"
},
"quality_considerations": {
"potential_weld_lines": self._identify_weld_line_risk(analysis),
"sink_mark_areas": self._identify_sink_mark_risk(analysis),
"warpage_risk": self._assess_warpage_risk(analysis)
}
}
return key_info
# ==================== 内部方法 ====================
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
"""分析产品几何属性"""
# 计算体积属性
volume_props = GProp_GProps()
brepgprop.VolumeProperties(shape, volume_props)
# 计算表面积属性
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
# 计算边界框
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
return {
"volume": volume_props.Mass(),
"surface_area": surface_props.Mass(),
"center_of_mass": [
volume_props.CentreOfMass().X(),
volume_props.CentreOfMass().Y(),
volume_props.CentreOfMass().Z()
],
"bounding_box": {
"min": [xmin, ymin, zmin],
"max": [xmax, ymax, zmax],
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
},
"inertia_matrix": self._get_inertia_matrix(volume_props)
}
def _detect_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
"""检测分型面和分型线"""
# 简化的分型面检测:基于Z方向的最高点和最低点
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
# 创建分型面(XY平面)
parting_plane = gp_Pln(
gp_Pnt(0, 0, center_z),
gp_Dir(0, 0, 1)
)
parting_surface = BRepBuilderAPI_MakeFace(
parting_plane,
bbox["min"][0] - 10, bbox["max"][0] + 10,
bbox["min"][1] - 10, bbox["max"][1] + 10
).Face()
# 分型线(简化)
parting_line = [
[bbox["min"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["min"][1], center_z]
]
return parting_surface, parting_line
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
"""应用收缩率补偿(放大模型)"""
scale_factor = 1.0 + self.shrinkage_rate
# 创建缩放变换
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
return scaled_shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""添加拔模角(简化实现)"""
# 实际实现需要复杂的拔模面处理
# 这里返回原始形状(假设已在CAD中处理)
logger.warning("拔模角处理为简化实现,建议在设计阶段处理")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""分离型腔和型芯"""
try:
# 使用分型面切割产品
# 上半部分为型腔(Cavity)
# 下半部分为型芯(Core)
# 这里需要实现BRepAlgoAPI_Section或类似的切割操作
# 简化:返回相同的形状(实际需实现切割逻辑)
return shape, shape # (cavity, core)
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return shape, shape
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
"""提取形状几何数据为JSON格式"""
try:
# 网格化
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
# 提取顶点和面
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.BRep import BRep_Tool
from OCC.Core.Poly import Poly_Triangulation
from OCC.Core.TopLoc import TopLoc_Location
vertices = []
faces = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
vertex_index = 0
while explorer.More():
# 使用 explorer.Current() 直接获取面
face = explorer.Current()
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
if triangulation:
# 提取顶点
nb_nodes = triangulation.NbNodes()
for i in range(1, nb_nodes + 1):
node = triangulation.Node(i)
# 应用位置变换
transformed = node.Transformed(location.Transformation())
vertices.extend([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z())
])
# 提取三角形面
nb_triangles = triangulation.NbTriangles()
for i in range(1, nb_triangles + 1):
triangle = triangulation.Triangle(i)
# 三角形顶点索引需要加上之前的顶点数量
idx1 = triangle.Value(1) + vertex_index - 1
idx2 = triangle.Value(2) + vertex_index - 1
idx3 = triangle.Value(3) + vertex_index - 1
faces.extend([int(idx1), int(idx2), int(idx3)])
vertex_index += nb_nodes
explorer.Next()
vertex_count = len(vertices) // 3
face_count = len(faces) // 3
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": vertex_count,
"face_count": face_count,
"triangulation": "BRepMesh三角化"
}
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0,
"triangulation": f"提取失败: {str(e)}"
}
def _extract_parting_surface_geometry(self, surface: Any) -> Dict[str, Any]:
"""提取分型面几何数据"""
# 尝试从surface获取边界信息,失败则使用默认值
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
adaptor = BRepAdaptor_Surface(surface)
u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
bounds = {
"u_range": [float(u_min), float(u_max)],
"v_range": [float(v_min), float(v_max)]
}
except Exception as e:
logger.warning(f"分型面边界提取失败,使用默认值: {e}")
bounds = {
"u_range": [-200, 200],
"v_range": [-200, 200]
}
# 分型面是水平面,法向量为 [0, 0, 1],原点在 Z 轴中心
return {
"type": "plane",
"normal": [0, 0, 1],
"origin": [0, 0, 0],
"bounds": bounds
}
return {
"type": "plane",
"normal": [0, 0, 1],
"origin": [0, 0, 0],
"bounds": bounds
}
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
"""估算模具尺寸"""
product_bbox = analysis["bounding_box"]["dimensions"]
# 模具通常比产品大20-50mm
margin = 30 # mm
return {
"length": product_bbox[0] + 2 * margin,
"width": product_bbox[1] + 2 * margin,
"height": product_bbox[2] + 2 * margin + 100, # 增加100mm用于模架
"margin": margin
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
"""估算锁模力"""
volume_cm3 = analysis.get("volume", 0) / 1000 # mm³ → cm³
# 经验公式: 锁模力 ≈ 投影面积 × 压力 × 安全系数
# 简化估算
if volume_cm3 < 10:
return "50-100 吨"
elif volume_cm3 < 100:
return "150-300 吨"
elif volume_cm3 < 500:
return "400-600 吨"
else:
return "800+ 吨"
def _get_recommended_material(self) -> str:
"""推荐模具材料 - 铝模具专用"""
return "Aluminum Alloy 7075 (铝合金模具)"
def _calculate_product_weight(self, analysis: Dict) -> str:
"""计算产品重量(泡沫材料,密度约0.1 g/cm³)"""
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * 0.1 # EPP泡沫密度约0.1 g/cm³
return f"{weight_g:.2f} g"
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
avg_thickness = (volume / surface_area) * 0.6
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
elif volume > 0:
# 如果没有surface_area,基于体积估算
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
efficiency = volume / bbox_volume
avg_thickness = (bbox_dims[0] + bbox_dims[1]) / 2 * efficiency
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
return "2.0 - 4.0 mm (默认)"
def _calculate_complexity_score(self, analysis: Dict) -> float:
"""计算复杂度评分(0-10)"""
# 基于体积、表面积比、边界框等
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
thickness_ratio = (volume / surface_area) * 0.6
complexity = min(thickness_ratio / 5.0, 10.0)
return round(complexity, 1)
elif volume > 0:
# 如果没有surface_area,基于拓扑复杂度评分
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
volume_ratio = volume / bbox_volume
complexity = (1.0 - volume_ratio) * 10
return round(min(max(complexity, 0), 10), 1)
return 5.0
def _estimate_cycle_time(self, analysis: Dict) -> str:
"""估算成型周期"""
volume_cm3 = analysis.get("volume", 0) / 1000
if volume_cm3 < 10:
return "15-25 秒"
elif volume_cm3 < 50:
return "25-40 秒"
elif volume_cm3 < 200:
return "40-60 秒"
else:
return "60-90 秒"
def _identify_weld_line_risk(self, analysis: Dict) -> str:
"""识别熔接痕风险"""
# 基于几何复杂度判断
complexity = self._calculate_complexity_score(analysis)
if complexity > 7:
return "高 - 建议优化浇口位置"
elif complexity > 4:
return "中 - 需仿真验证"
else:
return "低"
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
"""识别缩痕风险"""
thickness = self._estimate_wall_thickness(analysis)
# 简化的风险评估
return "中 - 建议壁厚均匀性检查"
def _assess_warpage_risk(self, analysis: Dict) -> str:
"""评估翘曲风险"""
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
aspect_ratio = max(bbox) / min(bbox)
if aspect_ratio > 5:
return "高 - 建议增加加强筋"
elif aspect_ratio > 3:
return "中 - 需优化冷却"
else:
return "低"
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
"""获取惯性矩阵"""
inertia = props.MatrixOfInertia()
return [
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
]
def _calculate_parting_line_length(self, parting_line: List) -> float:
"""计算分型线长度"""
# 简化的长度计算
return 250.0 # mm
+292
View File
@@ -0,0 +1,292 @@
# core/stp_parser.py
from pathlib import Path
from typing import Dict, Any, Optional, List
import numpy as np
import json
from utils.logger import get_logger
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
logger = get_logger(__name__)
class STPParser:
"""STP文件解析器"""
def __init__(self):
# 强制要求PythonOCC必须可用
self._verify_occ_availability()
def _verify_occ_availability(self):
"""验证PythonOCC是否可用,不可用则抛出异常"""
try:
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone
logger.info("PythonOCC验证通过")
except ImportError as e:
logger.error("PythonOCC不可用,服务无法运行")
raise RuntimeError("PythonOCC未安装,请安装PythonOCC后再运行服务") from e
def load_step_file(self, file_path: Path) -> Any:
"""加载STP文件"""
try:
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone
logger.info(f"加载STP文件: {file_path}")
reader = STEPControl_Reader()
status = reader.ReadFile(str(file_path))
if status == IFSelect_RetDone:
reader.TransferRoots()
shape = reader.OneShape()
logger.info("STP文件加载成功")
return shape
else:
raise ValueError(f"STP文件读取失败,状态码: {status}")
except Exception as e:
logger.error(f"STP解析失败: {e}")
raise
def analyze_geometry(self, shape) -> Dict[str, Any]:
"""分析几何属性"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
logger.info("开始几何分析...")
# 计算边界框
bbox = self._compute_bounding_box(shape)
# 计算体积和表面积
volume = self._compute_volume(shape)
area = self._compute_surface_area(shape)
# 分析拓扑
topology = self._analyze_topology(shape)
# 计算质心
center_of_mass = self._compute_center_of_mass(shape)
# 计算惯性属性
inertia_properties = self._compute_inertia_properties(shape)
result = {
"bounding_box": bbox,
"volume": float(volume),
"surface_area": float(area),
"topology": topology,
"center_of_mass": center_of_mass,
"inertia_properties": inertia_properties,
"analysis_method": "pythonocc"
}
logger.info("几何分析完成")
return result
except Exception as e:
logger.error(f"几何分析失败: {e}")
raise
def _compute_bounding_box(self, shape) -> Dict[str, Any]:
"""计算边界框"""
try:
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
return {
"min": [float(xmin), float(ymin), float(zmin)],
"max": [float(xmax), float(ymax), float(zmax)],
"dimensions": [
float(xmax - xmin),
float(ymax - ymin),
float(zmax - zmin)
],
"center": [
float((xmin + xmax) / 2),
float((ymin + ymax) / 2),
float((zmin + zmax) / 2)
]
}
except Exception as e:
logger.error(f"边界框计算失败: {e}")
return self._default_bounding_box()
def _compute_volume(self, shape) -> float:
"""计算体积"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
return props.Mass()
except Exception as e:
logger.error(f"体积计算失败: {e}")
return 1000000.0
def _compute_surface_area(self, shape) -> float:
"""计算表面积"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.SurfaceProperties(shape, props)
area = props.Mass()
logger.info(f"表面积计算成功: {area:.2f} mm²")
# 如果计算结果为0,使用备选估算方法
if area <= 0:
logger.warning("表面积计算结果为0,使用边界框估算")
raise ValueError("Surface area is zero")
return area
except Exception as e:
logger.error(f"表面积计算失败: {e}")
# 基于边界框估算表面积
try:
bbox = self._compute_bounding_box(shape)
dims = bbox.get("dimensions", [100, 100, 100])
# 简化的估算公式:2*(lw + lh + wh)
estimated_area = 2 * (dims[0]*dims[1] + dims[0]*dims[2] + dims[1]*dims[2])
logger.warning(f"使用边界框估算表面积: {estimated_area:.2f} mm²")
return estimated_area
except:
return 60000.0
def _compute_center_of_mass(self, shape) -> List[float]:
"""计算质心"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
center = props.CentreOfMass()
return [float(center.X()), float(center.Y()), float(center.Z())]
except Exception as e:
logger.error(f"质心计算失败: {e}")
return [0.0, 0.0, 0.0]
def _compute_inertia_properties(self, shape) -> Dict[str, Any]:
"""计算惯性属性"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
inertia = props.MatrixOfInertia()
return {
"mass": float(props.Mass()),
"moment_of_inertia": [
[float(inertia.Value(1, 1)), float(inertia.Value(1, 2)), float(inertia.Value(1, 3))],
[float(inertia.Value(2, 1)), float(inertia.Value(2, 2)), float(inertia.Value(2, 3))],
[float(inertia.Value(3, 1)), float(inertia.Value(3, 2)), float(inertia.Value(3, 3))]
]
}
except Exception as e:
logger.error(f"惯性属性计算失败: {e}")
return {}
def _analyze_topology(self, shape) -> Dict[str, int]:
"""分析拓扑"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
def count_elements(element_type):
explorer = TopExp_Explorer(shape, element_type)
count = 0
while explorer.More():
count += 1
explorer.Next()
return count
return {
"faces": count_elements(TopAbs_FACE),
"edges": count_elements(TopAbs_EDGE),
"vertices": count_elements(TopAbs_VERTEX)
}
except Exception as e:
logger.error(f"拓扑分析失败: {e}")
raise
def _create_dummy_shape(self):
"""创建虚拟形状"""
return "dummy_shape"
def _simulate_analysis(self) -> Dict[str, Any]:
"""模拟分析结果"""
logger.info("使用模拟分析数据")
return {
"bounding_box": self._default_bounding_box(),
"volume": 1000000.0,
"surface_area": 60000.0,
"topology": {"faces": 6, "edges": 12, "vertices": 8},
"center_of_mass": [50.0, 50.0, 50.0],
"inertia_properties": {},
"analysis_method": "simulated"
}
def _default_bounding_box(self) -> Dict[str, Any]:
"""默认边界框"""
return {
"min": [0.0, 0.0, 0.0],
"max": [100.0, 100.0, 100.0],
"dimensions": [100.0, 100.0, 100.0],
"center": [50.0, 50.0, 50.0]
}
def export_to_json(self, geometry_data: Dict[str, Any], output_path: Path) -> str:
"""将几何数据导出为JSON文件"""
try:
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 添加元数据
json_data = {
"metadata": {
"export_time": str(np.datetime64('now')),
"analysis_method": geometry_data.get("analysis_method", "unknown"),
"version": "1.0.0"
},
"geometry_data": geometry_data
}
# 保存JSON文件
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
logger.info(f"几何数据已导出到: {output_path}")
return str(output_path)
except Exception as e:
logger.error(f"JSON导出失败: {e}")
raise
def get_json_data(self, geometry_data: Dict[str, Any]) -> Dict[str, Any]:
"""获取JSON格式的几何数据"""
return {
"metadata": {
"export_time": str(np.datetime64('now')),
"analysis_method": geometry_data.get("analysis_method", "unknown"),
"version": "1.0.0"
},
"geometry_data": geometry_data
}
+99
View File
@@ -0,0 +1,99 @@
# database/database.py
import sys
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import text
from sqlalchemy.orm import sessionmaker
from config.settings import settings
import asyncio
from utils.logger import get_logger
logger = get_logger(__name__)
class DatabaseManager:
"""数据库管理器"""
def __init__(self):
self.engine = None
self.async_session = None
self.is_connected = False
async def connect(self):
"""连接数据库"""
if not settings.DATABASE_URL:
logger.warning("未配置数据库连接,跳过数据库初始化")
self.is_connected = False
return
try:
# 创建异步引擎
self.engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_size=20,
max_overflow=30,
pool_recycle=3600
)
# 创建异步会话工厂
self.async_session = async_sessionmaker(
self.engine,
class_=AsyncSession,
expire_on_commit=False
)
# 测试连接
async with self.engine.begin() as conn:
await conn.execute(text("SELECT 1"))
self.is_connected = True
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {e}")
self.is_connected = False
raise
async def disconnect(self):
"""断开数据库连接"""
if self.engine:
await self.engine.dispose()
self.is_connected = False
logger.info("数据库连接已断开")
async def get_session(self) -> AsyncSession:
"""获取数据库会话"""
if not self.is_connected:
await self.connect()
return self.async_session()
async def create_tables(self):
"""创建数据库表"""
from models.database import Base
try:
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("数据库表创建成功")
except Exception as e:
logger.error(f"数据库表创建失败: {e}")
raise
# 全局数据库管理器实例
db_manager = DatabaseManager()
# 数据库依赖注入
async def get_db_session():
"""获取数据库会话的依赖函数"""
session = await db_manager.get_session()
try:
yield session
finally:
await session.close()
+25
View File
@@ -0,0 +1,25 @@
# database/init_db.py
import asyncio
from database.database import db_manager
from utils.logger import get_logger
logger = get_logger(__name__)
async def init_database():
"""初始化数据库"""
try:
# 连接数据库
await db_manager.connect()
# 创建表
await db_manager.create_tables()
logger.info("数据库初始化完成")
return True
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
return False
if __name__ == "__main__":
asyncio.run(init_database())
+67
View File
@@ -0,0 +1,67 @@
"""数据库迁移脚本 - 删除旧表并重新创建"""
import asyncio
import sys
import os
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(Path(__file__).parent.parent))
# 设置环境变量确保正确导入
os.environ['PYTHONPATH'] = str(project_root) + os.pathsep + str(Path(__file__).parent.parent)
from src.database.database import db_manager
from src.models.database import Base
from src.utils.logger import get_logger
logger = get_logger(__name__)
async def migrate_database():
"""迁移数据库:删除所有表并重新创建"""
try:
# 连接数据库
await db_manager.connect()
# 删除所有表
logger.info("正在删除所有数据库表...")
async with db_manager.engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
# 重新创建所有表
logger.info("正在创建所有数据库表...")
async with db_manager.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("数据库迁移完成!")
return True
except Exception as e:
logger.error(f"数据库迁移失败: {e}")
return False
finally:
await db_manager.disconnect()
if __name__ == "__main__":
import sys
# 检查命令行参数
if len(sys.argv) > 1 and sys.argv[1] == '--force':
confirm = 'yes'
else:
print("=== 数据库迁移 ===")
print("警告:这将删除所有数据库表和数据!")
confirm = input("确认继续?(yes/no): ")
if confirm.lower() == 'yes':
asyncio.run(migrate_database())
else:
print("已取消迁移")
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - fsa30scy_tc-01-0817.stp</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
#container { position: relative; width: 100vw; height: 100vh; }
#canvas { display: block; }
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}
.metric { margin: 5px 0; }
.metric-label { font-weight: bold; color: #333; }
.metric-value { color: #666; }
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">fsa30scy_tc-01-0817.stp</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">1000000.00 mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">60000.00 mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">100.0 × 100.0 × 100.0 mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">6</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">12</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">8</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {
"bounding_box": {
"min": [
0.0,
0.0,
0.0
],
"max": [
100.0,
100.0,
100.0
],
"dimensions": [
100.0,
100.0,
100.0
],
"center": [
50.0,
50.0,
50.0
]
},
"volume": 1000000.0,
"surface_area": 60000.0,
"topology": {
"faces": 6,
"edges": 12,
"vertices": 8
},
"center_of_mass": [
50.0,
50.0,
50.0
],
"inertia_properties": {},
"analysis_method": "simulated"
};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制函数
function resetView() {
controls.reset();
}
function toggleWireframe() {
scene.traverse((child) => {
if (child.isMesh) {
child.material.wireframe = !child.material.wireframe;
}
});
}
</script>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
# main.py
import sys
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent
src_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
# 确保当前工作目录是项目根目录
os.chdir(project_root)
# 打印调试信息
print(f"项目根目录: {project_root}")
print(f"Python路径: {sys.path}")
print(f"当前工作目录: {os.getcwd()}")
# 测试导入配置模块
try:
from config.settings import settings
print("[OK] 配置模块导入成功")
except ImportError as e:
print(f"[FAIL] 配置模块导入失败: {e}")
# 列出当前目录内容
print("当前目录内容:")
for item in os.listdir('.'):
print(f" - {item}")
# 列出config目录内容
if os.path.exists('config'):
print("config目录内容:")
for item in os.listdir('config'):
print(f" - {item}")
raise
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import asyncio
from api.routes import router
from utils.logger import setup_logging
from database.init_db import init_database
# 设置日志
setup_logging()
# 创建FastAPI应用
app = FastAPI(
title="模具几何分析服务",
description="基于PythonOCC的STP文件几何分析和模具设计建议服务",
version="3.0.0"
)
# 启动时初始化数据库和RustFS
@app.on_event("startup")
async def startup_event():
"""应用启动时初始化数据库和RustFS"""
# 初始化数据库
success = await init_database()
if success:
print("[OK] 数据库初始化成功")
else:
print("[FAIL] 数据库初始化失败,服务将继续运行但数据库功能不可用")
# 初始化RustFS连接
try:
from storage.rustfs_storage import rustfs_manager
from config.settings import settings
await rustfs_manager.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
print("[OK] RustFS连接成功")
except Exception as e:
print(f"[FAIL] RustFS连接失败: {e}")
print("[WARN] 文件上传功能将不可用,但其他功能正常")
# 创建必要目录
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
TEMPLATES_DIR = Path("templates")
TEMPLATES_DIR.mkdir(exist_ok=True)
STATIC_DIR = Path("static")
STATIC_DIR.mkdir(exist_ok=True)
HTML_OUTPUT_DIR = Path("html_output")
HTML_OUTPUT_DIR.mkdir(exist_ok=True)
# 挂载静态文件
import os
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# 注册路由
app.include_router(router)
@app.get("/health")
async def health():
from database.database import db_manager
return {
"status": "healthy",
"service": "mold-geometry-analysis",
"database_connected": db_manager.is_connected
}
if __name__ == "__main__":
import uvicorn
# 从配置文件获取端口配置
from config.settings import settings
print("启动模具几何分析服务 v3.0...")
print(f"访问 http://localhost:{settings.PORT} 使用网页界面")
print("新增功能:")
print(" - STP文件解析为JSON数据")
print(" - 数据存储到PostgreSQL数据库")
print(" - 自动生成3D可视化HTML页面")
print(" - 源文件、JSON数据、HTML文件统一管理")
print(f"调试接口: http://localhost:{settings.PORT}/debug/tasks")
uvicorn.run(
"main:app",
host=settings.HOST,
port=settings.PORT,
reload=True
)
+328
View File
@@ -0,0 +1,328 @@
# models/database.py
from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, LargeBinary, Boolean, Float, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class User(Base):
"""用户表"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(255), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(100))
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=func.now())
last_login = Column(DateTime, nullable=True)
# 关联关系
stp_files = relationship("STPFile", back_populates="user")
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>"
class STPFile(Base):
"""STP源文件元数据表"""
__tablename__ = "stp_files"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
# 对象存储信息
object_key = Column(String(500), nullable=False, index=True) # MinIO对象键
storage_bucket = Column(String(100), nullable=False) # 存储桶名称
object_url = Column(String(1000), nullable=True) # 预签名URL(可选)
# 文件信息
original_filename = Column(String(255), nullable=False)
file_size = Column(Integer, nullable=False)
file_hash = Column(String(64), unique=True, index=True) # SHA256 hash
mime_type = Column(String(50), default="application/octet-stream")
# 时间戳
upload_time = Column(DateTime, default=func.now())
processed_time = Column(DateTime, nullable=True)
# 状态
status = Column(String(20), default="pending") # pending, processing, completed, failed
error_message = Column(Text, nullable=True)
# 保留旧字段以兼容
file_path = Column(String(500), nullable=True) # 本地路径(已弃用)
file_content = Column(LargeBinary, nullable=True) # 本地存储(已弃用)
filename = Column(String(255), nullable=True) # 已弃用
# 关联关系
user = relationship("User", back_populates="stp_files")
geometry_data = relationship("GeometryData", back_populates="stp_file", uselist=False)
mold_cavity_data = relationship("MoldCavityData", back_populates="stp_file", uselist=False)
html_file = relationship("HTMLFile", back_populates="stp_file", uselist=False)
def __repr__(self):
return f"<STPFile(id={self.id}, object_key='{self.object_key}', status='{self.status}')>"
class GeometryData(Base):
"""几何数据JSON元数据表"""
__tablename__ = "geometry_data"
id = Column(Integer, primary_key=True, index=True)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 对象存储信息
object_key = Column(String(500), nullable=False)
storage_bucket = Column(String(100), nullable=False)
object_url = Column(String(1000), nullable=True)
# 分析方法
analysis_method = Column(String(50), default="pythonocc") # pythonocc, simulated
# 时间戳
created_time = Column(DateTime, default=func.now())
# 几何属性摘要(便于快速查询)
volume = Column(Float, nullable=True)
surface_area = Column(Float, nullable=True)
bounding_box_min = Column(JSON, nullable=True)
bounding_box_max = Column(JSON, nullable=True)
center_of_mass = Column(JSON, nullable=True)
# 拓扑信息
topology_faces = Column(Integer, nullable=True)
topology_edges = Column(Integer, nullable=True)
topology_vertices = Column(Integer, nullable=True)
# 关联关系
stp_file = relationship("STPFile", back_populates="geometry_data")
def __repr__(self):
return f"<GeometryData(id={self.id}, stp_file_id={self.stp_file_id})>"
class HTMLFile(Base):
"""网页文件元数据表"""
__tablename__ = "html_files"
id = Column(Integer, primary_key=True, index=True)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 对象存储信息
object_key = Column(String(500), nullable=False)
storage_bucket = Column(String(100), nullable=False)
object_url = Column(String(1000), nullable=True)
# 文件信息
filename = Column(String(255), nullable=False)
generated_time = Column(DateTime, default=func.now())
# 可视化相关元数据
visualization_type = Column(String(50), default="3d_viewer")
has_interactive_elements = Column(Boolean, default=True)
# 保留旧字段以兼容
file_path = Column(String(500), nullable=True)
html_content = Column(Text, nullable=True)
# 关联关系
stp_file = relationship("STPFile", back_populates="html_file")
def __repr__(self):
return f"<HTMLFile(id={self.id}, stp_file_id={self.stp_file_id}, object_key='{self.object_key}')>"
class ProcessingTask(Base):
"""处理任务记录表"""
__tablename__ = "processing_tasks"
id = Column(Integer, primary_key=True, index=True)
task_id = Column(String(36), unique=True, index=True, nullable=False)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 任务类型和状态
task_type = Column(String(50), default="stp_parsing") # stp_parsing, geometry_analysis, mold_generation
status = Column(String(20), default="pending") # pending, processing, completed, failed
# 时间戳
created_time = Column(DateTime, default=func.now())
started_time = Column(DateTime, nullable=True)
completed_time = Column(DateTime, nullable=True)
# 处理进度
progress = Column(Integer, default=0) # 0-100
current_step = Column(String(100), nullable=True)
# 错误信息
error_message = Column(Text, nullable=True)
error_stack = Column(Text, nullable=True)
# 处理参数
parameters = Column(JSON, nullable=True) # 任务参数
def __repr__(self):
return f"<ProcessingTask(id={self.id}, task_id='{self.task_id}', status='{self.status}')>"
class MoldCavityData(Base):
"""模具型腔数据元数据表"""
__tablename__ = "mold_cavity_data"
id = Column(Integer, primary_key=True, index=True)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 对象存储信息
detailed_object_key = Column(String(500), nullable=False) # 完整三维数据
storage_bucket = Column(String(100), nullable=False)
# 模具类型和材料
mold_material = Column(String(100), default="Aluminum Alloy 7075")
mold_type = Column(String(50), default="single_cavity") # single_cavity, multi_cavity
# 工艺参数
shrinkage_rate = Column(Float, nullable=False)
draft_angle = Column(Float, nullable=False)
parting_line_length = Column(Float, nullable=True)
# 生成时间
generated_time = Column(DateTime, default=func.now())
# 关键信息摘要(快速查询字段)
cavity_key_info = Column(JSON, nullable=True) # 完整关键信息
# 提取的字段(便于查询和排序)
mold_size_length = Column(Float, nullable=True)
mold_size_width = Column(Float, nullable=True)
mold_size_height = Column(Float, nullable=True)
estimated_clamping_force = Column(String(50), nullable=True)
product_weight = Column(String(50), nullable=True)
product_volume = Column(Float, nullable=True)
wall_thickness_range = Column(String(50), nullable=True)
complexity_score = Column(Float, nullable=True)
# 质量评估
weld_line_risk = Column(String(50), nullable=True) # 熔接痕风险
sink_mark_risk = Column(String(50), nullable=True) # 缩痕风险
warpage_risk = Column(String(50), nullable=True) # 翘曲风险
# 关联关系
stp_file = relationship("STPFile", back_populates="mold_cavity_data")
def __repr__(self):
return f"<MoldCavityData(stp_file_id={self.stp_file_id}, mold_material='{self.mold_material}')>"
class FeatureDetection(Base):
"""特征检测结果表"""
__tablename__ = "feature_detections"
id = Column(Integer, primary_key=True, index=True)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 特征信息
feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, rib, boss, draft_angle
confidence = Column(Float, nullable=False) # 0.0 - 1.0
# 位置和尺寸
location = Column(JSON, nullable=True) # [x, y, z]
dimensions = Column(JSON, nullable=True) # [length, width, height]
# 特征参数
parameters = Column(JSON, nullable=True) # 自定义参数
# 检测时间
detected_at = Column(DateTime, default=func.now())
# 关联的几何数据
geometry_data_id = Column(Integer, ForeignKey("geometry_data.id"), nullable=True)
def __repr__(self):
return f"<FeatureDetection(id={self.id}, feature_type='{self.feature_type}', confidence={self.confidence})>"
class DesignRecommendation(Base):
"""设计建议表"""
__tablename__ = "design_recommendations"
id = Column(Integer, primary_key=True, index=True)
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 建议信息
rec_type = Column(String(50), nullable=False) # wall_thickness, draft_angle, etc.
priority = Column(String(20), nullable=False) # high, medium, low
description = Column(String(500), nullable=False)
reason = Column(Text, nullable=True)
# 建议参数
parameters = Column(JSON, nullable=True)
# 状态
status = Column(String(20), default="pending") # pending, accepted, rejected
user_notes = Column(Text, nullable=True)
# 时间戳
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, nullable=True)
def __repr__(self):
return f"<DesignRecommendation(id={self.id}, rec_type='{self.rec_type}', priority='{self.priority}')>"
class UserActivity(Base):
"""用户活动日志表"""
__tablename__ = "user_activities"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# 活动信息
activity_type = Column(String(50), nullable=False, index=True) # upload, view, download, delete, export
resource_type = Column(String(50), nullable=True) # stp_file, geometry_data, mold_cavity
resource_id = Column(Integer, nullable=True)
# 活动详情
description = Column(Text, nullable=True)
meta_data = Column(JSON, nullable=True)
# 时间戳
created_at = Column(DateTime, default=func.now(), index=True)
# IP和设备信息
ip_address = Column(String(45), nullable=True)
user_agent = Column(String(500), nullable=True)
def __repr__(self):
return f"<UserActivity(id={self.id}, user_id={self.user_id}, activity_type='{self.activity_type}')>"
class SystemLog(Base):
"""系统日志表(重要操作和错误)"""
__tablename__ = "system_logs"
id = Column(Integer, primary_key=True, index=True)
# 日志级别
level = Column(String(20), nullable=False, index=True) # INFO, WARNING, ERROR, CRITICAL
# 日志信息
message = Column(Text, nullable=False)
module = Column(String(100), nullable=True) # 模块名
function_name = Column(String(100), nullable=True)
# 时间戳
created_at = Column(DateTime, default=func.now(), index=True)
# 用户信息(如果有关联用户)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# 额外信息
request_id = Column(String(100), nullable=True) # 关联的请求ID
execution_time_ms = Column(Integer, nullable=True) # 执行时间
# 关联数据
resource_type = Column(String(50), nullable=True)
resource_id = Column(Integer, nullable=True)
def __repr__(self):
return f"<SystemLog(id={self.id}, level='{self.level}', module='{self.module}')>"
+135
View File
@@ -0,0 +1,135 @@
# models/schemas.py
from typing import Dict, List, Optional, Any
from enum import Enum
class ProcessingStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
# 简化的数据模型,避免复杂的Pydantic验证
def create_geometry_data(
bounding_box: Dict[str, List[float]],
volume: float,
surface_area: float,
topology: Dict[str, int],
analysis_method: str,
center_of_mass: Optional[List[float]] = None,
inertia_properties: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""创建几何数据"""
return {
"bounding_box": bounding_box,
"volume": volume,
"surface_area": surface_area,
"topology": topology,
"center_of_mass": center_of_mass or [0.0, 0.0, 0.0],
"inertia_properties": inertia_properties or {},
"analysis_method": analysis_method
}
def create_mold_feature(
feature_type: str,
confidence: float,
location: List[float],
dimensions: List[float],
parameters: Dict[str, Any],
recommendations: List[str]
) -> Dict[str, Any]:
"""创建模具特征"""
return {
"feature_type": feature_type,
"confidence": confidence,
"location": location,
"dimensions": dimensions,
"parameters": parameters,
"recommendations": recommendations
}
def create_design_recommendation(
rec_type: str,
priority: str,
description: str,
parameters: Dict[str, Any],
reason: str
) -> Dict[str, Any]:
"""创建设计建议"""
return {
"type": rec_type,
"priority": priority,
"description": description,
"parameters": parameters,
"reason": reason
}
def create_analysis_result(
geometry_data: Dict[str, Any],
detected_features: List[Dict[str, Any]],
design_recommendations: List[Dict[str, Any]],
quality_metrics: Dict[str, float],
analysis_summary: str
) -> Dict[str, Any]:
"""创建分析结果"""
return {
"geometry_data": geometry_data,
"detected_features": detected_features,
"design_recommendations": design_recommendations,
"quality_metrics": quality_metrics,
"analysis_summary": analysis_summary
}
def create_task_info(
task_id: str,
status: ProcessingStatus,
filename: str,
file_path: str,
file_size: int,
upload_time: str,
completed_at: Optional[str] = None,
geometry_data: Optional[Dict[str, Any]] = None,
analysis_result: Optional[Dict[str, Any]] = None,
error: Optional[str] = None
) -> Dict[str, Any]:
"""创建任务信息"""
return {
"task_id": task_id,
"status": status,
"filename": filename,
"file_path": file_path,
"file_size": file_size,
"upload_time": upload_time,
"completed_at": completed_at,
"geometry_data": geometry_data,
"analysis_result": analysis_result,
"error": error
}
# 添加到 schemas.py
def create_mold_cavity_data(
cavity_geometry: Dict[str, Any],
core_geometry: Dict[str, Any],
parting_surface: Dict[str, Any],
manufacturing_info: Dict[str, Any]
) -> Dict[str, Any]:
"""创建模具型腔详细数据"""
return {
"cavity_geometry": cavity_geometry,
"core_geometry": core_geometry,
"parting_surface": parting_surface,
"manufacturing_info": manufacturing_info
}
def create_mold_key_info(
mold_parameters: Dict[str, Any],
geometric_characteristics: Dict[str, Any],
manufacturing_requirements: Dict[str, Any],
quality_considerations: Dict[str, Any]
) -> Dict[str, Any]:
"""创建模具型腔关键信息"""
return {
"mold_parameters": mold_parameters,
"geometric_characteristics": geometric_characteristics,
"manufacturing_requirements": manufacturing_requirements,
"quality_considerations": quality_considerations
}
+1
View File
@@ -0,0 +1 @@
# Services 模块
+376
View File
@@ -0,0 +1,376 @@
# services/storage_integration.py
"""存储集成服务 - 协调 PostgreSQL 和 MinIO"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pathlib import Path
from typing import Optional, Dict, Any
import json
from models.database import (
STPFile, GeometryData, MoldCavityData,
HTMLFile, ProcessingTask, User,
FeatureDetection, DesignRecommendation,
UserActivity, SystemLog
)
from storage.object_storage import storage_manager
from utils.logger import get_logger
logger = get_logger(__name__)
class StorageIntegrationService:
"""存储集成服务"""
async def save_stp_file(self, session: AsyncSession,
file_path: Path,
original_filename: str,
user_id: Optional[int] = None) -> STPFile:
"""保存STP文件到PostgreSQL元数据 + MinIO对象存储"""
# 1. 上传到MinIO
upload_result = await storage_manager.upload_stp_file(
file_path,
original_filename
)
# 2. 创建PostgreSQL记录
stp_file = STPFile(
user_id=user_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['stp_files'],
original_filename=original_filename,
file_size=upload_result['file_size'],
file_hash=upload_result['file_hash'],
status="uploaded",
file_path=str(file_path) # 保留本地路径以兼容
)
session.add(stp_file)
await session.commit()
await session.refresh(stp_file)
logger.info(f"STP文件保存成功: {stp_file.id}")
return stp_file
async def save_geometry_data(self, session: AsyncSession,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str = "pythonocc") -> GeometryData:
"""保存几何数据到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_geometry_data(
geometry_json,
file_hash
)
# 3. 创建PostgreSQL记录
geometry_data = GeometryData(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['geometry_data'],
analysis_method=analysis_method,
# 提取摘要字段
volume=geometry_json.get('geometry_data', {}).get('volume'),
surface_area=geometry_json.get('geometry_data', {}).get('surface_area'),
bounding_box_min=geometry_json.get('geometry_data', {}).get('bounding_box', {}).get('min'),
bounding_box_max=geometry_json.get('geometry_data', {}).get('bounding_box', {}).get('max'),
center_of_mass=geometry_json.get('geometry_data', {}).get('center_of_mass'),
topology_faces=geometry_json.get('geometry_data', {}).get('topology', {}).get('faces'),
topology_edges=geometry_json.get('geometry_data', {}).get('topology', {}).get('edges'),
topology_vertices=geometry_json.get('geometry_data', {}).get('topology', {}).get('vertices')
)
session.add(geometry_data)
await session.commit()
await session.refresh(geometry_data)
logger.info(f"几何数据保存成功: {geometry_data.id}")
return geometry_data
async def save_mold_cavity_data(self, session: AsyncSession,
stp_file_id: int,
cavity_json: Dict[str, Any]) -> MoldCavityData:
"""保存模具型腔数据到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_mold_cavity_data(
cavity_json,
file_hash
)
# 3. 提取关键信息
metadata = cavity_json.get('metadata', {})
product_analysis = cavity_json.get('product_analysis', {})
manufacturing_info = cavity_json.get('manufacturing_info', {})
mold_size = manufacturing_info.get('estimated_mold_size', {})
key_info = cavity_json.get('mold_cavities', {}).get('cavity_key_info', {})
# 4. 创建PostgreSQL记录
mold_cavity = MoldCavityData(
stp_file_id=stp_file_id,
detailed_object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['mold_cavities'],
# 模具参数
mold_material=manufacturing_info.get('recommended_material', 'Aluminum Alloy 7075'),
shrinkage_rate=metadata.get('shrinkage_rate', 0.005),
draft_angle=metadata.get('draft_angle', 2.0),
# 提取的摘要字段
cavity_key_info=key_info,
mold_size_length=mold_size.get('length'),
mold_size_width=mold_size.get('width'),
mold_size_height=mold_size.get('height'),
estimated_clamping_force=manufacturing_info.get('estimated_clamping_force'),
product_volume=product_analysis.get('volume'),
# 从key_info中提取(如果存在)
product_weight=key_info.get('geometric_characteristics', {}).get('product_weight'),
wall_thickness_range=key_info.get('geometric_characteristics', {}).get('wall_thickness_range'),
complexity_score=key_info.get('geometric_characteristics', {}).get('complexity_score'),
# 质量评估
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
sink_mark_risk=key_info.get('quality_considerations', {}).get('sink_mark_areas'),
warpage_risk=key_info.get('quality_considerations', {}).get('warpage_risk')
)
session.add(mold_cavity)
await session.commit()
await session.refresh(mold_cavity)
logger.info(f"模具型腔数据保存成功: {mold_cavity.id}")
return mold_cavity
async def save_html_file(self, session: AsyncSession,
stp_file_id: int,
html_content: str,
filename: str) -> HTMLFile:
"""保存HTML文件到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_html_file(
html_content,
filename,
file_hash
)
# 3. 创建PostgreSQL记录
html_file = HTMLFile(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['html_files'],
filename=filename,
file_path=str(Path('html_output') / filename), # 保留本地路径
html_content=html_content # 保留内容以兼容
)
session.add(html_file)
await session.commit()
await session.refresh(html_file)
logger.info(f"HTML文件保存成功: {html_file.id}")
return html_file
async def save_features_and_recommendations(
self, session: AsyncSession,
stp_file_id: int,
features: list,
recommendations: list
):
"""保存特征检测结果和设计建议"""
# 1. 保存特征
for feature in features:
feature_record = FeatureDetection(
stp_file_id=stp_file_id,
feature_type=feature.get('feature_type'),
confidence=feature.get('confidence'),
location=feature.get('location'),
dimensions=feature.get('dimensions'),
parameters=feature.get('parameters')
)
session.add(feature_record)
# 2. 保存建议
for rec in recommendations:
rec_record = DesignRecommendation(
stp_file_id=stp_file_id,
rec_type=rec.get('rec_type'),
priority=rec.get('priority'),
description=rec.get('description'),
reason=rec.get('reason'),
parameters=rec.get('parameters')
)
session.add(rec_record)
await session.commit()
logger.info(f"保存了 {len(features)} 个特征和 {len(recommendations)} 个建议")
async def log_user_activity(self, session: AsyncSession,
user_id: int,
activity_type: str,
resource_type: Optional[str] = None,
resource_id: Optional[int] = None,
description: Optional[str] = None,
metadata: Optional[Dict] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None):
"""记录用户活动"""
activity = UserActivity(
user_id=user_id,
activity_type=activity_type,
resource_type=resource_type,
resource_id=resource_id,
description=description,
metadata=metadata,
ip_address=ip_address,
user_agent=user_agent
)
session.add(activity)
await session.commit()
logger.debug(f"用户活动记录: {activity_type} by user {user_id}")
async def get_stp_file_with_data(self, session: AsyncSession,
stp_file_id: int) -> Dict[str, Any]:
"""获取STP文件及其所有关联数据"""
# 1. 获取STP文件记录
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
result = {
'metadata': {
'id': stp_file.id,
'original_filename': stp_file.original_filename,
'file_size': stp_file.file_size,
'file_hash': stp_file.file_hash,
'upload_time': stp_file.upload_time.isoformat() if stp_file.upload_time else None,
'status': stp_file.status,
'user_id': stp_file.user_id
},
'geometry_data': None,
'mold_cavity_data': None,
'html_file': None,
'features': [],
'recommendations': []
}
# 2. 从MinIO获取数据
try:
# 几何数据
if stp_file.geometry_data:
geo_data_bytes = await storage_manager.download_file(
'geometry_data',
stp_file.geometry_data.object_key
)
result['geometry_data'] = json.loads(geo_data_bytes.decode('utf-8'))
# 模具型腔数据
if stp_file.mold_cavity_data:
cavity_data_bytes = await storage_manager.download_file(
'mold_cavities',
stp_file.mold_cavity_data.detailed_object_key
)
result['mold_cavity_data'] = json.loads(cavity_data_bytes.decode('utf-8'))
# HTML文件
if stp_file.html_file:
html_bytes = await storage_manager.download_file(
'html_files',
stp_file.html_file.object_key
)
result['html_content'] = html_bytes.decode('utf-8')
except Exception as e:
logger.error(f"从MinIO获取数据失败: {e}")
# 3. 从PostgreSQL获取特征和建议
features = await session.execute(
select(FeatureDetection).where(FeatureDetection.stp_file_id == stp_file_id)
)
result['features'] = [
{
'feature_type': f.feature_type,
'confidence': f.confidence,
'location': f.location,
'dimensions': f.dimensions,
'parameters': f.parameters
}
for f in features.scalars().all()
]
recommendations = await session.execute(
select(DesignRecommendation).where(DesignRecommendation.stp_file_id == stp_file_id)
)
result['recommendations'] = [
{
'rec_type': r.rec_type,
'priority': r.priority,
'description': r.description,
'reason': r.reason,
'parameters': r.parameters
}
for r in recommendations.scalars().all()
]
return result
async def delete_stp_file_cascade(self, session: AsyncSession,
stp_file_id: int):
"""级联删除STP文件及其所有关联数据"""
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
# 1. 删除MinIO中的文件
try:
if stp_file.object_key:
await storage_manager.delete_file('stp_files', stp_file.object_key)
except Exception as e:
logger.error(f"删除MinIO文件失败: {e}")
try:
if stp_file.geometry_data:
await storage_manager.delete_file('geometry_data', stp_file.geometry_data.object_key)
except Exception as e:
logger.error(f"删除几何数据失败: {e}")
try:
if stp_file.mold_cavity_data:
await storage_manager.delete_file('mold_cavities', stp_file.mold_cavity_data.detailed_object_key)
except Exception as e:
logger.error(f"删除型腔数据失败: {e}")
try:
if stp_file.html_file:
await storage_manager.delete_file('html_files', stp_file.html_file.object_key)
except Exception as e:
logger.error(f"删除HTML文件失败: {e}")
# 2. 级联删除PostgreSQL记录(通过外键自动处理)
await session.delete(stp_file)
await session.commit()
logger.info(f"STP文件及其关联数据已删除: {stp_file_id}")
# 全局存储集成服务实例
storage_integration = StorageIntegrationService()
+498
View File
@@ -0,0 +1,498 @@
# services/storage_integration_rustfs.py
"""存储集成服务 - 协调 PostgreSQL 和 RustFS"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from pathlib import Path
from typing import Optional, Dict, Any
import json
from datetime import datetime
from models.database import (
STPFile, GeometryData, MoldCavityData,
HTMLFile, ProcessingTask, User,
FeatureDetection, DesignRecommendation,
UserActivity, SystemLog
)
from storage.rustfs_storage import rustfs_manager
from utils.logger import get_logger
logger = get_logger(__name__)
class StorageIntegrationService:
"""存储集成服务 - PostgreSQL + RustFS"""
async def save_stp_file(self, session: AsyncSession,
file_path: Path,
original_filename: str,
user_id: Optional[int] = None) -> STPFile:
"""保存STP文件到PostgreSQL元数据 + RustFS对象存储"""
# 1. 上传到RustFS
upload_result = await rustfs_manager.upload_file(
file_type='stp_files',
file_path=file_path,
original_filename=original_filename,
metadata={
'original_filename': original_filename,
'user_id': str(user_id) if user_id else 'anonymous'
}
)
file_hash = upload_result['file_hash']
# 2. 检查是否已存在相同文件
existing_file = await session.execute(
select(STPFile).where(STPFile.file_hash == file_hash)
)
existing_file = existing_file.scalar_one_or_none()
if existing_file:
logger.info(f"文件已存在,返回现有记录: {existing_file.id}")
return existing_file
# 3. 创建新PostgreSQL记录
stp_file = STPFile(
user_id=user_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
original_filename=original_filename,
file_size=upload_result['file_size'],
file_hash=file_hash,
status="uploaded",
file_path=str(file_path) # 保留本地路径以兼容
)
session.add(stp_file)
await session.commit()
await session.refresh(stp_file)
logger.info(f"STP文件保存成功 RustFS: {stp_file.id}")
return stp_file
async def create_processing_task(self, session: AsyncSession,
task_id: str,
stp_file_id: int,
task_type: str = "stp_parsing") -> ProcessingTask:
"""创建处理任务记录"""
try:
task = ProcessingTask(
task_id=task_id,
stp_file_id=stp_file_id,
task_type=task_type,
status="pending",
started_time=datetime.now()
)
session.add(task)
await session.commit()
await session.refresh(task)
logger.info(f"处理任务创建成功: {task_id}")
return task
except Exception as e:
await session.rollback()
logger.error(f"创建处理任务失败: {e}")
raise
async def update_task_status(
self,
session: AsyncSession,
task_id: str,
status: str,
progress: Optional[int] = None,
current_step: Optional[str] = None,
error_message: Optional[str] = None
):
"""更新任务状态"""
try:
update_data = {
"status": status,
"completed_time": datetime.now() if status in ["completed", "failed"] else None,
"error_message": error_message
}
if progress is not None:
update_data["progress"] = progress
if current_step is not None:
update_data["current_step"] = current_step
await session.execute(
update(ProcessingTask)
.where(ProcessingTask.task_id == task_id)
.values(**update_data)
)
await session.commit()
logger.info(f"任务状态更新: {task_id} -> {status}")
except Exception as e:
await session.rollback()
logger.error(f"更新任务状态失败: {e}")
raise
async def update_stp_file_status(self, session: AsyncSession, stp_file_id: int, status: str):
"""更新STP文件状态"""
try:
await session.execute(
update(STPFile)
.where(STPFile.id == stp_file_id)
.values(
status=status,
processed_time=datetime.now() if status in ["completed", "failed"] else None
)
)
await session.commit()
logger.info(f"STP文件状态更新: ID {stp_file_id} -> {status}")
except Exception as e:
await session.rollback()
logger.error(f"更新STP文件状态失败: {e}")
raise
async def save_geometry_data(self, session: AsyncSession,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str = "pythonocc") -> GeometryData:
"""保存几何数据到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到RustFS
upload_result = await rustfs_manager.upload_json_data(
file_type='geometry_data',
json_data=geometry_json,
file_hash=file_hash
)
# 3. 提取几何数据
if 'geometry_data' in geometry_json:
geo_data = geometry_json['geometry_data']
else:
geo_data = geometry_json
# 4. 创建PostgreSQL记录
geometry_data = GeometryData(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
analysis_method=analysis_method,
# 提取摘要字段
volume=geo_data.get('volume'),
surface_area=geo_data.get('surface_area'),
bounding_box_min=geo_data.get('bounding_box', {}).get('min'),
bounding_box_max=geo_data.get('bounding_box', {}).get('max'),
center_of_mass=geo_data.get('center_of_mass'),
topology_faces=geo_data.get('topology', {}).get('faces'),
topology_edges=geo_data.get('topology', {}).get('edges'),
topology_vertices=geo_data.get('topology', {}).get('vertices')
)
session.add(geometry_data)
await session.commit()
await session.refresh(geometry_data)
logger.info(f"几何数据保存成功 RustFS: {geometry_data.id}")
return geometry_data
async def save_mold_cavity_data(self, session: AsyncSession,
stp_file_id: int,
cavity_json: Dict[str, Any]) -> MoldCavityData:
"""保存模具型腔数据到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到RustFS
upload_result = await rustfs_manager.upload_json_data(
file_type='mold_cavities',
json_data=cavity_json,
file_hash=file_hash
)
# 3. 提取关键信息
metadata = cavity_json.get('metadata', {})
product_analysis = cavity_json.get('product_analysis', {})
manufacturing_info = cavity_json.get('manufacturing_info', {})
mold_size = manufacturing_info.get('estimated_mold_size', {})
key_info = cavity_json.get('mold_cavities', {}).get('cavity_key_info', {})
# 4. 创建PostgreSQL记录
mold_cavity = MoldCavityData(
stp_file_id=stp_file_id,
detailed_object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
# 模具参数
mold_material=manufacturing_info.get('recommended_material', 'Aluminum Alloy 7075'),
shrinkage_rate=metadata.get('shrinkage_rate', 0.005),
draft_angle=metadata.get('draft_angle', 2.0),
# 提取的摘要字段
cavity_key_info=key_info,
mold_size_length=mold_size.get('length'),
mold_size_width=mold_size.get('width'),
mold_size_height=mold_size.get('height'),
estimated_clamping_force=manufacturing_info.get('estimated_clamping_force'),
product_volume=product_analysis.get('volume'),
# 从key_info中提取(如果存在)
product_weight=key_info.get('geometric_characteristics', {}).get('product_weight'),
wall_thickness_range=key_info.get('geometric_characteristics', {}).get('wall_thickness_range'),
complexity_score=key_info.get('geometric_characteristics', {}).get('complexity_score'),
# 质量评估
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
sink_mark_risk=key_info.get('quality_considerations', {}).get('sink_mark_areas'),
warpage_risk=key_info.get('quality_considerations', {}).get('warpage_risk')
)
session.add(mold_cavity)
await session.commit()
await session.refresh(mold_cavity)
logger.info(f"模具型腔数据保存成功 RustFS: {mold_cavity.id}")
return mold_cavity
async def save_html_file(self, session: AsyncSession,
stp_file_id: int,
filename: str,
file_path: str,
html_content: Optional[str] = None,
visualization_type: str = "3d_viewer") -> HTMLFile:
"""保存HTML文件到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 读取HTML内容(如果未提供)
if html_content is None:
try:
with open(file_path, 'r', encoding='utf-8') as f:
html_content = f.read()
except Exception as e:
logger.error(f"读取HTML文件失败: {e}")
html_content = ""
# 3. 上传到RustFS
html_json = {'content': html_content, 'filename': filename}
upload_result = await rustfs_manager.upload_json_data(
file_type='html_files',
json_data=html_json,
file_hash=file_hash
)
# 4. 创建PostgreSQL记录
html_file = HTMLFile(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
filename=filename,
file_path=file_path, # 保留本地路径
html_content=html_content, # 保留内容以兼容
visualization_type=visualization_type
)
session.add(html_file)
await session.commit()
await session.refresh(html_file)
logger.info(f"HTML文件保存成功 RustFS: {html_file.id}")
return html_file
async def save_features_and_recommendations(
self, session: AsyncSession,
stp_file_id: int,
features: list,
recommendations: list
):
"""保存特征检测结果和设计建议"""
# 1. 保存特征
for feature in features:
feature_record = FeatureDetection(
stp_file_id=stp_file_id,
feature_type=feature.get('feature_type'),
confidence=feature.get('confidence'),
location=feature.get('location'),
dimensions=feature.get('dimensions'),
parameters=feature.get('parameters')
)
session.add(feature_record)
# 2. 保存建议
for rec in recommendations:
rec_record = DesignRecommendation(
stp_file_id=stp_file_id,
rec_type=rec.get('rec_type'),
priority=rec.get('priority'),
description=rec.get('description'),
reason=rec.get('reason'),
parameters=rec.get('parameters')
)
session.add(rec_record)
await session.commit()
logger.info(f"保存了 {len(features)} 个特征和 {len(recommendations)} 个建议")
async def log_user_activity(self, session: AsyncSession,
user_id: int,
activity_type: str,
resource_type: Optional[str] = None,
resource_id: Optional[int] = None,
description: Optional[str] = None,
metadata: Optional[Dict] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None):
"""记录用户活动"""
activity = UserActivity(
user_id=user_id,
activity_type=activity_type,
resource_type=resource_type,
resource_id=resource_id,
description=description,
metadata=metadata,
ip_address=ip_address,
user_agent=user_agent
)
session.add(activity)
await session.commit()
logger.debug(f"用户活动记录: {activity_type} by user {user_id}")
async def get_stp_file_with_data(self, session: AsyncSession,
stp_file_id: int) -> Dict[str, Any]:
"""获取STP文件及其所有关联数据"""
# 1. 获取STP文件记录
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
result = {
'metadata': {
'id': stp_file.id,
'original_filename': stp_file.original_filename,
'file_size': stp_file.file_size,
'file_hash': stp_file.file_hash,
'upload_time': stp_file.upload_time.isoformat() if stp_file.upload_time else None,
'status': stp_file.status,
'user_id': stp_file.user_id
},
'geometry_data': None,
'mold_cavity_data': None,
'html_content': None,
'features': [],
'recommendations': []
}
# 2. 从RustFS获取数据
try:
# 几何数据
if stp_file.geometry_data:
geo_data_bytes = await rustfs_manager.download_file(
file_type='geometry_data',
object_key=stp_file.geometry_data.object_key
)
result['geometry_data'] = json.loads(geo_data_bytes.decode('utf-8'))
# 模具型腔数据
if stp_file.mold_cavity_data:
cavity_data_bytes = await rustfs_manager.download_file(
file_type='mold_cavities',
object_key=stp_file.mold_cavity_data.detailed_object_key
)
result['mold_cavity_data'] = json.loads(cavity_data_bytes.decode('utf-8'))
# HTML文件
if stp_file.html_file:
html_bytes = await rustfs_manager.download_file(
file_type='html_files',
object_key=stp_file.html_file.object_key
)
html_json = json.loads(html_bytes.decode('utf-8'))
result['html_content'] = html_json.get('content', '')
except Exception as e:
logger.error(f"从RustFS获取数据失败: {e}")
# 3. 从PostgreSQL获取特征和建议
features = await session.execute(
select(FeatureDetection).where(FeatureDetection.stp_file_id == stp_file_id)
)
result['features'] = [
{
'feature_type': f.feature_type,
'confidence': f.confidence,
'location': f.location,
'dimensions': f.dimensions,
'parameters': f.parameters
}
for f in features.scalars().all()
]
recommendations = await session.execute(
select(DesignRecommendation).where(DesignRecommendation.stp_file_id == stp_file_id)
)
result['recommendations'] = [
{
'rec_type': r.rec_type,
'priority': r.priority,
'description': r.description,
'reason': r.reason,
'parameters': r.parameters
}
for r in recommendations.scalars().all()
]
return result
async def delete_stp_file_cascade(self, session: AsyncSession,
stp_file_id: int):
"""级联删除STP文件及其所有关联数据"""
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
# 1. 删除RustFS中的文件
try:
if stp_file.object_key:
await rustfs_manager.delete_file('stp_files', stp_file.object_key)
except Exception as e:
logger.error(f"删除RustFS文件失败: {e}")
try:
if stp_file.geometry_data:
await rustfs_manager.delete_file('geometry_data', stp_file.geometry_data.object_key)
except Exception as e:
logger.error(f"删除几何数据失败: {e}")
try:
if stp_file.mold_cavity_data:
await rustfs_manager.delete_file('mold_cavities', stp_file.mold_cavity_data.detailed_object_key)
except Exception as e:
logger.error(f"删除型腔数据失败: {e}")
try:
if stp_file.html_file:
await rustfs_manager.delete_file('html_files', stp_file.html_file.object_key)
except Exception as e:
logger.error(f"删除HTML文件失败: {e}")
# 2. 级联删除PostgreSQL记录(通过外键自动处理)
await session.delete(stp_file)
await session.commit()
logger.info(f"STP文件及其关联数据已删除: {stp_file_id}")
# 全局存储集成服务实例
storage_integration = StorageIntegrationService()
+296
View File
@@ -0,0 +1,296 @@
# services/storage_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from datetime import datetime
import hashlib
import json
from pathlib import Path
from typing import Optional, Dict, Any
from models.database import STPFile, GeometryData, HTMLFile, ProcessingTask
from utils.logger import get_logger
from models.database import MoldCavityData
logger = get_logger(__name__)
class StorageService:
"""数据存储服务"""
def __init__(self, db_session: AsyncSession):
self.db_session = db_session
async def save_stp_file(
self,
filename: str,
file_path: str,
file_size: int,
file_content: Optional[bytes] = None
) -> STPFile:
"""保存STP文件信息到数据库"""
try:
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path, file_content)
# 检查是否已存在相同文件
existing_file = await self.db_session.execute(
select(STPFile).where(STPFile.file_hash == file_hash)
)
existing_file = existing_file.scalar_one_or_none()
if existing_file:
logger.info(f"文件已存在,跳过保存: {filename}")
return existing_file
# 创建新的STP文件记录
stp_file = STPFile(
filename=filename,
original_filename=filename,
file_path=file_path,
file_size=file_size,
file_hash=file_hash,
file_content=file_content,
upload_time=datetime.now(),
status="pending",
# 必填字段提供默认值
object_key=f"stp_files/{file_hash}",
storage_bucket="default",
object_url=None
)
self.db_session.add(stp_file)
await self.db_session.commit()
await self.db_session.refresh(stp_file)
logger.info(f"STP文件保存成功: {filename} (ID: {stp_file.id})")
return stp_file
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存STP文件失败: {e}")
raise
async def save_geometry_data(
self,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str
) -> GeometryData:
"""保存几何数据JSON到数据库"""
try:
# 提取关键几何属性用于快速查询
volume = geometry_json.get("volume")
surface_area = geometry_json.get("surface_area")
bounding_box = geometry_json.get("bounding_box", {})
geometry_data = GeometryData(
stp_file_id=stp_file_id,
analysis_method=analysis_method,
volume=volume,
surface_area=surface_area,
bounding_box_min=bounding_box.get("min"),
bounding_box_max=bounding_box.get("max"),
created_time=datetime.now(),
# 必填字段提供默认值
object_key=f"geometry_data/{stp_file_id}",
storage_bucket="default",
object_url=None
)
self.db_session.add(geometry_data)
await self.db_session.commit()
await self.db_session.refresh(geometry_data)
logger.info(f"几何数据保存成功: STP文件ID {stp_file_id}")
return geometry_data
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存几何数据失败: {e}")
raise
async def save_html_file(
self,
stp_file_id: int,
filename: str,
file_path: str,
html_content: Optional[str] = None,
visualization_type: str = "3d_viewer"
) -> HTMLFile:
"""保存HTML文件信息到数据库"""
try:
html_file = HTMLFile(
stp_file_id=stp_file_id,
filename=filename,
file_path=file_path,
html_content=html_content,
visualization_type=visualization_type,
has_interactive_elements=True,
generated_time=datetime.now(),
# 必填字段提供默认值
object_key=f"html_files/{stp_file_id}",
storage_bucket="default",
object_url=None
)
self.db_session.add(html_file)
await self.db_session.commit()
await self.db_session.refresh(html_file)
logger.info(f"HTML文件保存成功: {filename} (STP文件ID: {stp_file_id})")
return html_file
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存HTML文件失败: {e}")
raise
async def create_processing_task(
self,
task_id: str,
stp_file_id: int,
task_type: str = "stp_parsing"
) -> ProcessingTask:
"""创建处理任务记录"""
try:
task = ProcessingTask(
task_id=task_id,
stp_file_id=stp_file_id,
task_type=task_type,
status="pending",
started_time=datetime.now()
)
self.db_session.add(task)
await self.db_session.commit()
await self.db_session.refresh(task)
logger.info(f"处理任务创建成功: {task_id}")
return task
except Exception as e:
await self.db_session.rollback()
logger.error(f"创建处理任务失败: {e}")
raise
async def update_task_status(
self,
task_id: str,
status: str,
progress: Optional[int] = None,
current_step: Optional[str] = None,
error_message: Optional[str] = None
):
"""更新任务状态"""
try:
update_data = {
"status": status,
"completed_time": datetime.now() if status in ["completed", "failed"] else None,
"error_message": error_message
}
if progress is not None:
update_data["progress"] = progress
if current_step is not None:
update_data["current_step"] = current_step
await self.db_session.execute(
update(ProcessingTask)
.where(ProcessingTask.task_id == task_id)
.values(**update_data)
)
await self.db_session.commit()
logger.info(f"任务状态更新: {task_id} -> {status}")
except Exception as e:
await self.db_session.rollback()
logger.error(f"更新任务状态失败: {e}")
raise
async def update_stp_file_status(self, stp_file_id: int, status: str):
"""更新STP文件状态"""
try:
await self.db_session.execute(
update(STPFile)
.where(STPFile.id == stp_file_id)
.values(
status=status,
processed_time=datetime.now() if status in ["completed", "failed"] else None
)
)
await self.db_session.commit()
logger.info(f"STP文件状态更新: ID {stp_file_id} -> {status}")
except Exception as e:
await self.db_session.rollback()
logger.error(f"更新STP文件状态失败: {e}")
raise
async def get_stp_file_by_id(self, stp_file_id: int) -> Optional[STPFile]:
"""根据ID获取STP文件"""
try:
result = await self.db_session.execute(
select(STPFile).where(STPFile.id == stp_file_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"获取STP文件失败: {e}")
return None
async def get_geometry_data_by_stp_file_id(self, stp_file_id: int) -> Optional[GeometryData]:
"""根据STP文件ID获取几何数据"""
try:
result = await self.db_session.execute(
select(GeometryData).where(GeometryData.stp_file_id == stp_file_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"获取几何数据失败: {e}")
return None
def _calculate_file_hash(self, file_path: str, file_content: Optional[bytes] = None) -> str:
"""计算文件哈希值"""
sha256_hash = hashlib.sha256()
if file_content:
sha256_hash.update(file_content)
else:
# 从文件路径读取内容计算哈希
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
async def save_mold_cavity_data(
self,
stp_file_id: int,
cavity_json: Dict[str, Any],
key_info: Dict[str, Any]
) -> MoldCavityData:
"""保存模具型腔数据"""
try:
mold_data = MoldCavityData(
stp_file_id=stp_file_id,
cavity_key_info=key_info,
shrinkage_rate=cavity_json["metadata"]["shrinkage_rate"],
draft_angle=cavity_json["metadata"]["draft_angle"],
generated_time=datetime.now(),
# 必填字段提供默认值
detailed_object_key=f"mold_cavity/{stp_file_id}",
storage_bucket="default"
)
self.db_session.add(mold_data)
await self.db_session.commit()
await self.db_session.refresh(mold_data)
logger.info(f"模具型腔数据保存成功: STP文件ID {stp_file_id}")
return mold_data
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存模具型腔数据失败: {e}")
raise
+5
View File
@@ -0,0 +1,5 @@
# storage/__init__.py
from .rustfs_storage import RustFSManager, rustfs_manager
__all__ = ['RustFSManager', 'rustfs_manager']
+107
View File
@@ -0,0 +1,107 @@
# storage/init_storage.py
"""初始化 RustFS 对象存储"""
import asyncio
import sys
from pathlib import Path
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent.parent.parent
src_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from storage.rustfs_storage import rustfs_manager
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
async def init_rustfs_storage():
"""初始化 RustFS 对象存储"""
try:
# 连接到 RustFS (S3v4 API)
await rustfs_manager.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
logger.info("RustFS 对象存储初始化完成")
return True
except Exception as e:
logger.error(f"RustFS 对象存储初始化失败: {e}")
return False
async def test_storage():
"""测试 RustFS 对象存储功能"""
try:
import json
# 测试上传 JSON
test_data = {"test": True, "timestamp": "2024-01-01", "storage": "rustfs"}
result = await rustfs_manager.upload_json_data(
file_type='stp_files',
json_data=test_data,
file_hash='test-hash'
)
logger.info(f"RustFS 测试上传成功: {result['object_key']}")
# 测试下载
downloaded_bytes = await rustfs_manager.download_file(
file_type='stp_files',
object_key=result['object_key']
)
downloaded_data = json.loads(downloaded_bytes.decode('utf-8'))
logger.info(f"RustFS 测试下载成功: {downloaded_data}")
# 测试预签名 URL
url = await rustfs_manager.generate_presigned_url(
file_type='stp_files',
object_key=result['object_key'],
expires=3600
)
logger.info(f"RustFS 预签名URL: {url}")
# 清理测试文件
await rustfs_manager.delete_file(
file_type='stp_files',
object_key=result['object_key']
)
logger.info("RustFS 测试文件已清理")
return True
except Exception as e:
logger.error(f"RustFS 存储测试失败: {e}")
return False
if __name__ == "__main__":
async def main():
try:
print("=== 初始化 RustFS 对象存储 ===")
# 初始化存储
init_result = await init_rustfs_storage()
if init_result:
print("[OK] RustFS 连接成功")
print("\n=== 测试 RustFS 功能 ===")
# 运行测试
test_result = await test_storage()
if test_result:
print("[OK] RustFS 测试全部通过")
else:
print("[FAIL] RustFS 测试失败")
finally:
# 关闭连接
await rustfs_manager.close()
print("\n=== 连接已关闭 ===")
# 运行主函数
asyncio.run(main())
+361
View File
@@ -0,0 +1,361 @@
# storage/object_storage.py
"""MinIO/S3 对象存储服务"""
from minio import Minio
from minio.error import S3Error
from pathlib import Path
from typing import Optional, BinaryIO
from io import BytesIO
from utils.logger import get_logger
import hashlib
import uuid
logger = get_logger(__name__)
class ObjectStorageManager:
"""对象存储管理器 - MinIO/S3兼容"""
def __init__(self):
self.client: Optional[Minio] = None
self.is_connected = False
# 桶名称
self.buckets = {
'stp_files': 'moldinsight-stp-files', # STP/STEP文件
'geometry_data': 'moldinsight-geometry', # 几何数据JSON
'mold_cavities': 'moldinsight-mold-cavities', # 模具型腔数据
'html_files': 'moldinsight-html', # HTML报告文件
'user_files': 'moldinsight-user-files' # 用户上传的其他文件
}
async def connect(self, endpoint: str, access_key: str, secret_key: str,
secure: bool = False):
"""连接到MinIO/S3服务"""
try:
self.client = Minio(
endpoint,
access_key=access_key,
secret_key=secret_key,
secure=secure
)
# 测试连接
self.client.list_buckets()
self.is_connected = True
logger.info(f"对象存储连接成功: {endpoint}")
# 确保所有桶都存在
await self._ensure_buckets()
except S3Error as e:
logger.error(f"对象存储连接失败: {e}")
self.is_connected = False
raise
async def _ensure_buckets(self):
"""确保所有必要的桶都存在"""
for bucket_name in self.buckets.values():
try:
if not self.client.bucket_exists(bucket_name):
self.client.make_bucket(bucket_name)
logger.info(f"创建存储桶: {bucket_name}")
else:
logger.debug(f"存储桶已存在: {bucket_name}")
except S3Error as e:
logger.error(f"创建存储桶失败 {bucket_name}: {e}")
def _generate_object_key(self, original_filename: str, prefix: str = '') -> str:
"""生成对象存储的唯一键名"""
# 提取文件扩展名
ext = Path(original_filename).suffix
# 生成唯一ID
unique_id = str(uuid.uuid4())
# 生成键名: prefix/unique_id + original_ext
if prefix:
return f"{prefix}/{unique_id}{ext}"
return f"{unique_id}{ext}"
async def upload_stp_file(self, file_path: Path,
original_filename: str) -> dict:
"""上传STP文件到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['stp_files']
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path)
# 检查是否已存在
existing_key = await self._find_file_by_hash(bucket_name, file_hash)
if existing_key:
logger.info(f"文件已存在,跳过上传: {existing_key}")
return {
'object_key': existing_key,
'file_hash': file_hash,
'already_exists': True
}
# 生成唯一键名
object_key = self._generate_object_key(
original_filename,
prefix='stp'
)
# 上传文件
try:
result = self.client.fput_object(
bucket_name,
object_key,
str(file_path),
content_type='application/octet-stream'
)
logger.info(f"STP文件上传成功: {object_key}")
return {
'object_key': object_key,
'file_hash': file_hash,
'file_size': result.size,
'etag': result.etag,
'already_exists': False
}
except S3Error as e:
logger.error(f"STP文件上传失败: {e}")
raise
async def upload_geometry_data(self, geometry_json: dict,
file_hash: str) -> dict:
"""上传几何数据JSON到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['geometry_data']
# 使用文件哈希作为键名的一部分
object_key = f"geometry/{file_hash}.json"
# 转换为字节
import json
json_bytes = json.dumps(geometry_json, ensure_ascii=False).encode('utf-8')
# 上传
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
logger.info(f"几何数据上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"几何数据上传失败: {e}")
raise
async def upload_mold_cavity_data(self, cavity_json: dict,
file_hash: str) -> dict:
"""上传模具型腔数据到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['mold_cavities']
object_key = f"mold-cavity/{file_hash}.json"
import json
json_bytes = json.dumps(cavity_json, ensure_ascii=False).encode('utf-8')
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
logger.info(f"模具型腔数据上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"模具型腔数据上传失败: {e}")
raise
async def upload_html_file(self, html_content: str,
original_filename: str,
file_hash: str) -> dict:
"""上传HTML文件到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['html_files']
object_key = f"html/{file_hash}.html"
html_bytes = html_content.encode('utf-8')
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(html_bytes),
length=len(html_bytes),
content_type='text/html; charset=utf-8'
)
logger.info(f"HTML文件上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"HTML文件上传失败: {e}")
raise
async def download_file(self, bucket_type: str,
object_key: str) -> bytes:
"""从对象存储下载文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
response = self.client.get_object(bucket_name, object_key)
data = response.read()
response.close()
response.release_conn()
logger.debug(f"文件下载成功: {object_key}")
return data
except S3Error as e:
logger.error(f"文件下载失败 {object_key}: {e}")
raise
async def get_presigned_url(self, bucket_type: str,
object_key: str,
expires: int = 3600) -> str:
"""生成预签名URL(临时访问链接)"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
url = self.client.presigned_get_object(
bucket_name,
object_key,
expires=expires
)
return url
except S3Error as e:
logger.error(f"生成预签名URL失败: {e}")
raise
async def delete_file(self, bucket_type: str, object_key: str):
"""删除对象存储中的文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
self.client.remove_object(bucket_name, object_key)
logger.info(f"文件删除成功: {object_key}")
except S3Error as e:
logger.error(f"文件删除失败 {object_key}: {e}")
raise
def _calculate_file_hash(self, file_path: Path) -> str:
"""计算文件的SHA256哈希"""
sha256_hash = hashlib.sha256()
with open(file_path, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
async def _find_file_by_hash(self, bucket_name: str,
file_hash: str) -> Optional[str]:
"""根据哈希查找已存在的文件"""
try:
objects = self.client.list_objects(bucket_name, recursive=True)
for obj in objects:
# 从对象键中提取哈希(如果有)
if file_hash in obj.object_name:
return obj.object_name
return None
except S3Error as e:
logger.warning(f"查找文件哈希失败: {e}")
return None
async def get_file_info(self, bucket_type: str,
object_key: str) -> dict:
"""获取文件信息"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
stat = self.client.stat_object(bucket_name, object_key)
return {
'size': stat.size,
'etag': stat.etag,
'content_type': stat.content_type,
'last_modified': stat.last_modified
}
except S3Error as e:
logger.error(f"获取文件信息失败: {e}")
raise
async def list_files(self, bucket_type: str,
prefix: str = '') -> list:
"""列出存储桶中的文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
objects = self.client.list_objects(bucket_name, prefix=prefix)
return [
{
'object_key': obj.object_name,
'size': obj.size,
'etag': obj.etag,
'last_modified': obj.last_modified
}
for obj in objects
]
except S3Error as e:
logger.error(f"列出文件失败: {e}")
raise
# 全局对象存储管理器实例
storage_manager = ObjectStorageManager()
+348
View File
@@ -0,0 +1,348 @@
# storage/rustfs_storage.py
"""RustFS 对象存储服务 (S3v4 API 兼容)"""
from minio import Minio
from minio.error import S3Error
from pathlib import Path
from typing import Optional, Dict, Any
from io import BytesIO
from utils.logger import get_logger
from datetime import timedelta
import hashlib
import uuid
logger = get_logger(__name__)
class RustFSManager:
"""RustFS 对象存储管理器 (使用 MinIO S3 客户端)"""
def __init__(self, project_name: str = "moldinsight"):
self.client: Optional[Minio] = None
self.is_connected = False
self.project_name = project_name
# 使用单个项目桶,按类型组织文件
self.bucket_name = f"{project_name}"
# 文件类型前缀(子目录结构)
self.file_types = {
'stp_files': 'stp-files',
'geometry_data': 'geometry',
'mold_cavities': 'mold-cavities',
'html_files': 'html',
'user_files': 'user-files'
}
async def connect(self, endpoint: str, access_key: str, secret_key: str, timeout: int = 30):
"""连接到 RustFS 服务"""
try:
# 提取端口号和主机
from urllib.parse import urlparse
parsed = urlparse(endpoint)
host = parsed.netloc or parsed.path
# 创建 MinIO 客户端(S3v4 兼容)
self.client = Minio(
host,
access_key=access_key,
secret_key=secret_key,
secure=False, # HTTP 而不是 HTTPS
region='us-east-1'
)
# 测试连接
self.client.list_buckets()
self.is_connected = True
logger.info(f"RustFS 连接成功: {endpoint}")
# 确保所有桶都存在
await self._ensure_buckets()
except S3Error as e:
logger.error(f"RustFS 连接失败: {e}")
self.is_connected = False
raise
except Exception as e:
logger.error(f"RustFS 初始化失败: {e}")
self.is_connected = False
raise
async def close(self):
"""关闭连接"""
# MinIO 客户端不需要显式关闭
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def _ensure_buckets(self):
"""确保项目存储桶存在"""
try:
if not self.client.bucket_exists(self.bucket_name):
self.client.make_bucket(self.bucket_name)
logger.info(f"创建项目存储桶: {self.bucket_name}")
else:
logger.debug(f"项目存储桶已存在: {self.bucket_name}")
except S3Error as e:
logger.error(f"创建存储桶失败 {self.bucket_name}: {e}")
def _generate_object_key(self, original_filename: str, file_type: str = '') -> str:
"""生成对象存储的唯一键名"""
ext = Path(original_filename).suffix
unique_id = str(uuid.uuid4())
# 格式: {文件类型}/{唯一ID}.扩展名 (去掉项目名前缀)
if file_type and file_type in self.file_types:
type_prefix = self.file_types[file_type]
return f"{type_prefix}/{unique_id}{ext}"
# 默认格式
return f"misc/{unique_id}{ext}"
def _calculate_file_hash(self, file_path: Path) -> str:
"""计算文件的SHA256哈希"""
sha256_hash = hashlib.sha256()
with open(file_path, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
async def upload_file(self, file_type: str, file_path: Path,
original_filename: str,
metadata: Optional[Dict] = None) -> Dict[str, Any]:
"""上传文件到 RustFS"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path)
# 生成唯一键名
object_key = self._generate_object_key(original_filename, file_type)
# 上传文件
try:
result = self.client.fput_object(
self.bucket_name,
object_key,
str(file_path),
content_type='application/octet-stream',
metadata=metadata or {}
)
logger.info(f"文件上传成功 RustFS: {self.bucket_name}/{object_key}")
# 获取文件大小
file_size = file_path.stat().st_size
return {
'object_key': object_key,
'bucket': self.bucket_name,
'file_hash': file_hash,
'file_size': file_size,
'etag': result.etag if hasattr(result, 'etag') else None
}
except S3Error as e:
logger.error(f"RustFS 上传失败: {e}")
raise
async def upload_json_data(self, file_type: str,
json_data: Dict[str, Any],
file_hash: str) -> Dict[str, Any]:
"""上传JSON数据到 RustFS"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 格式: {文件类型}/{文件哈希}.json (去掉项目名前缀)
type_prefix = self.file_types[file_type]
object_key = f"{type_prefix}/{file_hash}.json"
# 转换为字节
import json
json_bytes = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
try:
result = self.client.put_object(
self.bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
logger.info(f"JSON数据上传成功 RustFS: {self.bucket_name}/{object_key}")
return {
'object_key': object_key,
'bucket': self.bucket_name,
'file_size': len(json_bytes),
'etag': result.etag if hasattr(result, 'etag') else None
}
except S3Error as e:
logger.error(f"RustFS JSON上传失败: {e}")
raise
async def download_file(self, file_type: str, object_key: str) -> bytes:
"""从 RustFS 下载文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
try:
response = self.client.get_object(self.bucket_name, object_key)
data = response.read()
response.close()
response.release_conn()
logger.debug(f"文件下载成功: {self.bucket_name}/{object_key}")
return data
except S3Error as e:
logger.error(f"RustFS 下载失败: {e}")
raise
async def get_file_info(self, file_type: str, object_key: str) -> Dict[str, Any]:
"""获取文件信息"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
try:
stat = self.client.stat_object(self.bucket_name, object_key)
return {
'size': stat.size,
'etag': stat.etag,
'content_type': stat.content_type,
'last_modified': stat.last_modified
}
except S3Error as e:
logger.error(f"RustFS 获取文件信息失败: {e}")
raise
async def delete_file(self, file_type: str, object_key: str):
"""删除 RustFS 中的文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
try:
self.client.remove_object(self.bucket_name, object_key)
logger.info(f"文件删除成功: {self.bucket_name}/{object_key}")
except S3Error as e:
logger.error(f"RustFS 删除失败: {e}")
raise
async def list_files(self, file_type: str, prefix: str = '') -> list:
"""列出存储桶中的文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 构建完整前缀:{文件类型}/... (去掉项目名前缀)
type_prefix = self.file_types[file_type]
full_prefix = f"{type_prefix}/"
if prefix:
full_prefix += prefix
try:
objects = self.client.list_objects(self.bucket_name, prefix=full_prefix, recursive=True)
return [
{
'object_key': obj.object_name,
'size': obj.size,
'etag': obj.etag,
'last_modified': obj.last_modified
}
for obj in objects
]
except S3Error as e:
logger.error(f"RustFS 列出文件失败: {e}")
raise
async def generate_presigned_url(self, file_type: str,
object_key: str,
expires: int = 3600,
method: str = 'GET') -> str:
"""生成预签名URL(临时访问链接)"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
try:
url = self.client.presigned_get_object(
self.bucket_name,
object_key,
expires=timedelta(seconds=expires)
)
return url
except S3Error as e:
logger.error(f"RustFS 生成预签名URL失败: {e}")
raise
async def file_exists(self, file_type: str, object_key: str) -> bool:
"""检查文件是否存在"""
try:
await self.get_file_info(file_type, object_key)
return True
except:
return False
async def get_storage_stats(self) -> Dict[str, Any]:
"""获取存储统计信息"""
try:
buckets = self.client.list_buckets()
total_objects = 0
total_size = 0
namespace_stats = {}
for bucket in buckets:
objects = self.client.list_objects(bucket.name, recursive=True)
bucket_count = 0
bucket_size = 0
for obj in objects:
bucket_count += 1
bucket_size += obj.size
namespace_stats[bucket.name] = {
'object_count': bucket_count,
'total_size': bucket_size
}
total_objects += bucket_count
total_size += bucket_size
return {
'total_objects': total_objects,
'total_size': total_size,
'namespace_stats': namespace_stats
}
except S3Error as e:
logger.error(f"RustFS 获取统计信息失败: {e}")
raise
# 全局 RustFS 管理器实例
rustfs_manager = RustFSManager()
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
# Utils 模块
+28
View File
@@ -0,0 +1,28 @@
# utils/file_handler.py
import aiofiles
from pathlib import Path
from fastapi import UploadFile
class FileHandler:
def __init__(self, upload_dir: str = "uploads"):
self.upload_dir = Path(upload_dir)
self.upload_dir.mkdir(exist_ok=True)
async def save_uploaded_file(self, file: UploadFile) -> Path:
"""保存上传的文件"""
file_path = self.upload_dir / file.filename
async with aiofiles.open(file_path, 'wb') as f:
content = await file.read()
await f.write(content)
return file_path
def cleanup_file(self, file_path: Path):
"""清理文件"""
try:
if file_path.exists():
file_path.unlink()
except Exception as e:
print(f"文件清理失败: {e}")
+279
View File
@@ -0,0 +1,279 @@
# utils/html_generator.py
from pathlib import Path
from typing import Dict, Any, Optional
import json
from datetime import datetime
from utils.logger import get_logger
logger = get_logger(__name__)
class HTMLGenerator:
"""HTML文件生成器"""
def __init__(self, output_dir: str = "./html_output"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
def generate_3d_viewer_html(
self,
geometry_data: Dict[str, Any],
stp_filename: str,
cavity_data: Optional[Dict[str, Any]] = None,
key_info: Optional[Dict[str, Any]] = None
) -> str:
"""生成3D可视化HTML页面"""
# 提取几何数据
bounding_box = geometry_data.get("bounding_box", {})
volume = geometry_data.get("volume", 0) or 0
surface_area = geometry_data.get("surface_area", 0) or 0
topology = geometry_data.get("topology", {})
center_of_mass = geometry_data.get("center_of_mass", [0, 0, 0])
cavity_html = ""
if cavity_data and key_info:
cavity_html = f"""
<div id="cavity-info-panel" style="position: absolute; top: 10px; right: 10px;">
<h3>🔧 模具型腔信息</h3>
<div class="metric">
<span class="metric-label">收缩率:</span>
<span class="metric-value">{cavity_data["metadata"]["shrinkage_rate"]}</span>
</div>
<div class="metric">
<span class="metric-label">拔模角:</span>
<span class="metric-value">{cavity_data["metadata"]["draft_angle"]}°</span>
</div>
<div class="metric">
<span class="metric-label">预估模具尺寸:</span>
<span class="metric-value">
{key_info["mold_parameters"]["mold_size"]["length"]:.0f} ×
{key_info["mold_parameters"]["mold_size"]["width"]:.0f} ×
{key_info["mold_parameters"]["mold_size"]["height"]:.0f} mm
</span>
</div>
<div class="metric">
<span class="metric-label">预估锁模力:</span>
<span class="metric-value">
{key_info["manufacturing_requirements"]["clamping_force"]}
</span>
</div>
<div class="metric">
<span class="metric-label">产品重量:</span>
<span class="metric-value">
{key_info["geometric_characteristics"]["product_weight"]}
</span>
</div>
</div>
"""
html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模具几何可视化 - {stp_filename}</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<style>
body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; }}
#container {{ position: relative; width: 100vw; height: 100vh; }}
#canvas {{ display: block; }}
#info-panel {{
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
#controls {{
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
}}
.metric {{ margin: 5px 0; }}
.metric-label {{ font-weight: bold; color: #333; }}
.metric-value {{ color: #666; }}
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
{cavity_html}
<div id="info-panel">
<h3>模具几何信息</h3>
<div class="metric">
<span class="metric-label">文件名:</span>
<span class="metric-value">{stp_filename}</span>
</div>
<div class="metric">
<span class="metric-label">体积:</span>
<span class="metric-value">{volume:.2f} mm³</span>
</div>
<div class="metric">
<span class="metric-label">表面积:</span>
<span class="metric-value">{surface_area:.2f} mm²</span>
</div>
<div class="metric">
<span class="metric-label">边界框:</span>
<span class="metric-value">{bounding_box.get('dimensions', [0, 0, 0])[0]:.1f} × {bounding_box.get('dimensions', [0, 0, 0])[1]:.1f} × {bounding_box.get('dimensions', [0, 0, 0])[2]:.1f} mm</span>
</div>
<div class="metric">
<span class="metric-label">面数:</span>
<span class="metric-value">{topology.get('faces', 0)}</span>
</div>
<div class="metric">
<span class="metric-label">边数:</span>
<span class="metric-value">{topology.get('edges', 0)}</span>
</div>
<div class="metric">
<span class="metric-label">顶点数:</span>
<span class="metric-value">{topology.get('vertices', 0)}</span>
</div>
</div>
<div id="controls">
<button onclick="resetView()">重置视图</button>
<button onclick="toggleWireframe()">切换线框</button>
</div>
</div>
<script>
// 初始化Three.js场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({{ canvas: document.getElementById('canvas') }});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xf0f0f0);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
// 创建几何体(模拟模具形状)
const geometryData = {json.dumps(geometry_data, indent=2)};
// 根据边界框创建模拟几何体
const bbox = geometryData.bounding_box;
if (bbox) {{
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
// 创建基础几何体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({{
color: 0x4CAF50,
transparent: true,
opacity: 0.8,
wireframe: false
}});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 添加线框
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add(line);
}}
// 设置相机位置
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 动画循环
function animate() {{
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}});
// 控制函数
function resetView() {{
controls.reset();
}}
function toggleWireframe() {{
scene.traverse((child) => {{
if (child.isMesh) {{
child.material.wireframe = !child.material.wireframe;
}}
}});
}}
</script>
</body>
</html>
"""
return html_content
def save_html_file(self, html_content: str, filename: str) -> str:
"""保存HTML文件到磁盘"""
try:
file_path = self.output_dir / filename
with open(file_path, 'w', encoding='utf-8') as f:
f.write(html_content)
logger.info(f"HTML文件保存成功: {file_path}")
return str(file_path)
except Exception as e:
logger.error(f"保存HTML文件失败: {e}")
raise
def generate_and_save_visualization(
self,
geometry_data: Dict[str, Any],
stp_filename: str
) -> str:
"""生成并保存可视化HTML文件"""
try:
# 生成HTML内容
html_content = self.generate_3d_viewer_html(geometry_data, stp_filename)
# 创建文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = stp_filename.replace('.', '_').replace(' ', '_')
html_filename = f"{safe_filename}_{timestamp}.html"
# 保存文件
file_path = self.save_html_file(html_content, html_filename)
return file_path
except Exception as e:
logger.error(f"生成可视化文件失败: {e}")
raise
+17
View File
@@ -0,0 +1,17 @@
# utils/logger.py
import logging
import sys
def setup_logging():
"""设置日志配置"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
def get_logger(name: str):
"""获取日志器"""
return logging.getLogger(name)
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# MoldInsight 启动脚本 - 修复版
# 适用于Linux conda环境
echo "🚀 启动 MoldInsight 模具几何分析系统..."
# 检查Python版本
python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
echo "📋 Python版本: $python_version"
# 初始化conda环境
echo "🔧 初始化conda环境..."
source /opt/anaconda3/etc/profile.d/conda.sh
# 激活conda环境
echo "🔧 激活conda环境..."
conda activate moldinsight
# 验证PythonOCC是否可用
echo "🔍 验证PythonOCC..."
python3 -c "import OCC; print('✅ PythonOCC可用')" || {
echo "❌ PythonOCC不可用,请先安装PythonOCC"
exit 1
}
# 检查是否已经安装依赖
echo "📦 检查依赖包..."
pip install -r requirements.txt
# 创建必要目录
echo "📁 创建必要目录..."
mkdir -p uploads html_output logs
# 检查环境配置文件
if [ ! -f ".env" ]; then
echo "❌ 错误: 未找到.env配置文件"
echo "📋 请创建.env文件并配置数据库连接信息:"
echo " 1. 创建.env文件: touch .env"
echo " 2. 编辑配置文件: nano .env"
echo " 3. 添加数据库配置:"
echo " DATABASE_URL=postgresql+asyncpg://username:password@localhost:5432/database_name"
echo ""
echo "💡 提示: 请根据实际的PostgreSQL连接信息修改上述配置"
exit 1
fi
echo "🔧 检查数据库连接..."
# 启动服务
echo "🚀 启动 MoldInsight 服务..."
PORT=$(grep '^PORT=' .env 2>/dev/null | cut -d'=' -f2 || echo '8000')
echo "🌐 服务将在 http://localhost:${PORT} 启动"
echo "📊 功能特性:"
echo " - STP文件解析和几何分析"
echo " - JSON数据导出"
echo " - PostgreSQL数据库存储"
echo " - 3D可视化HTML生成"
echo " - Web界面文件上传"
echo " - 任务状态跟踪"
echo ""
echo "按 Ctrl+C 停止服务"
echo ""
# 启动应用
python3 src/main.py
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# MoldInsight 启动脚本 - 修复版
# 适用于Linux conda环境
echo "🚀 启动 MoldInsight 模具几何分析系统..."
# 检查Python版本
python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
echo "📋 Python版本: $python_version"
# 初始化conda环境
echo "🔧 初始化conda环境..."
source /opt/anaconda3/etc/profile.d/conda.sh
# 激活conda环境
echo "🔧 激活conda环境..."
conda activate moldinsight
# 验证PythonOCC是否可用
echo "🔍 验证PythonOCC..."
python3 -c "import OCC; print('✅ PythonOCC可用')" || {
echo "❌ PythonOCC不可用,请先安装PythonOCC"
exit 1
}
# 检查是否已经安装依赖
echo "📦 检查依赖包..."
pip install -r requirements.txt
# 创建必要目录
echo "📁 创建必要目录..."
mkdir -p uploads html_output logs
# 检查环境配置文件
if [ ! -f ".env" ]; then
echo "❌ 错误: 未找到.env配置文件"
echo "📋 请创建.env文件并配置数据库连接信息:"
echo " 1. 创建.env文件: touch .env"
echo " 2. 编辑配置文件: nano .env"
echo " 3. 添加数据库配置:"
echo " DATABASE_URL=postgresql+asyncpg://username:password@localhost:5432/database_name"
echo ""
echo "💡 提示: 请根据实际的PostgreSQL连接信息修改上述配置"
exit 1
fi
echo "🔧 检查数据库连接..."
# 启动服务
echo "🚀 启动 MoldInsight 服务..."
PORT=$(grep '^PORT=' .env 2>/dev/null | cut -d'=' -f2 || echo '8000')
echo "🌐 服务将在 http://localhost:${PORT} 启动"
echo "📊 功能特性:"
echo " - STP文件解析和几何分析"
echo " - JSON数据导出"
echo " - PostgreSQL数据库存储"
echo " - 3D可视化HTML生成"
echo " - Web界面文件上传"
echo " - 任务状态跟踪"
echo ""
echo "按 Ctrl+C 停止服务"
echo ""
# 启动应用
python3 src/main.py
+600
View File
@@ -0,0 +1,600 @@
// static/script.js
let selectedFile = null;
const uploadSection = document.getElementById('uploadSection');
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const loading = document.getElementById('loading');
const resultsSection = document.getElementById('resultsSection');
const errorMessage = document.getElementById('errorMessage');
const taskInfo = document.getElementById('taskInfo');
const geometryData = document.getElementById('geometryData');
const boundingBoxData = document.getElementById('boundingBoxData');
const topologyData = document.getElementById('topologyData');
const featuresData = document.getElementById('featuresData');
const recommendationsData = document.getElementById('recommendationsData');
const metricsData = document.getElementById('metricsData');
const analysisInfo = document.getElementById('analysisInfo');
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成');
showUploadSection();
});
// 显示上传区域
function showUploadSection() {
uploadSection.style.display = 'block';
resultsSection.style.display = 'none';
resetUploadArea();
}
// 显示结果区域
function showResultsSection() {
uploadSection.style.display = 'none';
resultsSection.style.display = 'block';
}
// 重置上传区域
function resetUploadArea() {
selectedFile = null;
uploadArea.innerHTML = `
<div class="upload-icon">📁</div>
<h3>拖放文件到此处或点击选择</h3>
<p>最大文件大小: 100MB</p>
<input type="file" id="fileInput" class="file-input" accept=".stp,.step">
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
选择文件
</button>
`;
uploadBtn.disabled = true;
hideError();
loading.style.display = 'none';
// 重新绑定事件
const newFileInput = document.getElementById('fileInput');
newFileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
}
// 拖放功能
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files[0]);
}
});
// 文件选择
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
function handleFileSelect(file) {
if (!file.name.toLowerCase().endsWith('.stp') && !file.name.toLowerCase().endsWith('.step')) {
showError('请选择STP或STEP格式的文件');
return;
}
if (file.size > 100 * 1024 * 1024) {
showError('文件大小不能超过100MB');
return;
}
selectedFile = file;
uploadArea.innerHTML = `
<div class="upload-icon">✅</div>
<h3>已选择文件</h3>
<p><strong>${file.name}</strong></p>
<p>大小: ${(file.size / 1024 / 1024).toFixed(2)} MB</p>
`;
uploadBtn.disabled = false;
hideError();
}
async function uploadFile() {
if (!selectedFile) return;
loading.style.display = 'block';
uploadBtn.disabled = true;
hideError();
const formData = new FormData();
formData.append('file', selectedFile);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
const result = await response.json();
console.log('上传结果:', result);
// 开始轮询任务状态
pollTaskStatus(result.task_id);
} catch (error) {
showError('上传失败: ' + error.message);
loading.style.display = 'none';
uploadBtn.disabled = false;
}
}
async function pollTaskStatus(taskId) {
try {
const response = await fetch(`/status/${taskId}`);
const task = await response.json();
console.log('任务状态:', task.status);
console.log('完整任务数据:', task);
updateTaskInfo(task);
if (task.status === 'completed') {
loading.style.display = 'none';
showResultsSection();
displayAllResults(task);
} else if (task.status === 'failed') {
loading.style.display = 'none';
showError('分析失败: ' + (task.error || '未知错误'));
uploadBtn.disabled = false;
} else {
setTimeout(() => pollTaskStatus(taskId), 1000);
}
} catch (error) {
console.error('轮询错误:', error);
loading.style.display = 'none';
showError('查询状态失败: ' + error.message);
uploadBtn.disabled = false;
}
}
function updateTaskInfo(task) {
taskInfo.innerHTML = `
<div class="data-item">
<div class="data-label">📋 任务ID</div>
<div class="data-value">${task.task_id || 'N/A'}</div>
</div>
<div class="data-item">
<div class="data-label">🔄 状态</div>
<div class="data-value">
<span class="status-badge status-${task.status}">${getStatusText(task.status)}</span>
</div>
</div>
<div class="data-item">
<div class="data-label">📁 文件名</div>
<div class="data-value">${task.filename || 'N/A'}</div>
</div>
<div class="data-item">
<div class="data-label">📏 文件大小</div>
<div class="data-value">${task.file_size ? formatFileSize(task.file_size) : 'N/A'}</div>
</div>
`;
}
function displayAllResults(task) {
console.log('显示所有结果:', task);
displayGeometryData(task);
displayBoundingBoxData(task);
displayTopologyData(task);
// 添加模具型腔数据显示
if (task.cavity_data) {
displayCavityData(task.cavity_data);
}
if (task.key_info) {
displayKeyInfo(task.key_info);
}
displayFeaturesData(task);
displayRecommendationsData(task);
displayMetricsData(task);
displayAnalysisInfo(task);
}
function displayGeometryData(task) {
if (task.geometry_data) {
const geo = task.geometry_data;
geometryData.innerHTML = `
<div class="data-item">
<div class="data-label">📦 体积</div>
<div class="data-value">
${geo.volume ? formatNumber(geo.volume) : 'N/A'}
<span class="data-unit">mm³</span>
</div>
</div>
<div class="data-item">
<div class="data-label">📐 表面积</div>
<div class="data-value">
${geo.surface_area ? formatNumber(geo.surface_area) : 'N/A'}
<span class="data-unit">mm²</span>
</div>
</div>
<div class="data-item">
<div class="data-label">📏 体积表面积比</div>
<div class="data-value">
${geo.volume && geo.surface_area ? (geo.volume / geo.surface_area).toFixed(4) : 'N/A'}
</div>
</div>
`;
} else {
geometryData.innerHTML = '<div class="data-item"><div class="data-value">无几何数据</div></div>';
}
}
function displayBoundingBoxData(task) {
if (task.geometry_data && task.geometry_data.bounding_box) {
const bbox = task.geometry_data.bounding_box;
boundingBoxData.innerHTML = `
<div class="data-item coordinate-item">
<div class="coordinate-label">📍 最小坐标</div>
<div class="coordinate-value">
X: ${bbox.min[0].toFixed(2)}<br>
Y: ${bbox.min[1].toFixed(2)}<br>
Z: ${bbox.min[2].toFixed(2)}
</div>
</div>
<div class="data-item coordinate-item">
<div class="coordinate-label">📍 最大坐标</div>
<div class="coordinate-value">
X: ${bbox.max[0].toFixed(2)}<br>
Y: ${bbox.max[1].toFixed(2)}<br>
Z: ${bbox.max[2].toFixed(2)}
</div>
</div>
<div class="data-item">
<div class="data-label">📏 尺寸</div>
<div class="data-value">
${bbox.dimensions[0].toFixed(2)} × ${bbox.dimensions[1].toFixed(2)} × ${bbox.dimensions[2].toFixed(2)}
<span class="data-unit">mm</span>
</div>
</div>
`;
} else {
boundingBoxData.innerHTML = '<div class="data-item"><div class="data-value">无边界框数据</div></div>';
}
}
function displayTopologyData(task) {
if (task.geometry_data && task.geometry_data.topology) {
const topo = task.geometry_data.topology;
topologyData.innerHTML = `
<div class="data-item">
<div class="data-label">🔺 面数</div>
<div class="data-value">${topo.faces || 0}</div>
</div>
<div class="data-item">
<div class="data-label">📏 边数</div>
<div class="data-value">${topo.edges || 0}</div>
</div>
<div class="data-item">
<div class="data-label">📍 顶点数</div>
<div class="data-value">${topo.vertices || 0}</div>
</div>
<div class="data-item">
<div class="data-label">📊 拓扑复杂度</div>
<div class="data-value">${calculateTopologyComplexity(topo)}</div>
</div>
`;
} else {
topologyData.innerHTML = '<div class="data-item"><div class="data-value">无拓扑数据</div></div>';
}
}
function displayFeaturesData(task) {
if (task.analysis_result && task.analysis_result.detected_features) {
const features = task.analysis_result.detected_features;
if (features.length > 0) {
featuresData.innerHTML = features.map(feature => `
<div class="feature-item">
<div class="feature-header">
<div class="feature-type">${getFeatureTypeText(feature.feature_type)}</div>
<div class="confidence-badge">置信度: ${(feature.confidence * 100).toFixed(0)}%</div>
</div>
<div class="data-item">
<div class="data-label">📍 位置</div>
<div class="data-value">${feature.location.map(v => v.toFixed(2)).join(', ')}</div>
</div>
<div class="data-item">
<div class="data-label">📏 尺寸</div>
<div class="data-value">${feature.dimensions.map(v => v.toFixed(2)).join(' × ')} mm</div>
</div>
${feature.recommendations && feature.recommendations.length > 0 ? `
<div class="data-item">
<div class="data-label">💡 建议</div>
<ul class="recommendation-list">
${feature.recommendations.map(rec => `<li>${rec}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`).join('');
} else {
featuresData.innerHTML = '<div class="data-item"><div class="data-value">未检测到明显特征</div></div>';
}
} else {
featuresData.innerHTML = '<div class="data-item"><div class="data-value">无特征数据</div></div>';
}
}
function displayRecommendationsData(task) {
if (task.analysis_result && task.analysis_result.design_recommendations) {
const recommendations = task.analysis_result.design_recommendations;
if (recommendations.length > 0) {
recommendationsData.innerHTML = recommendations.map(rec => `
<div class="recommendation-item recommendation-${rec.priority}">
<div class="recommendation-header">
<div class="feature-type">${getRecommendationTypeText(rec.type)}</div>
<div class="priority-badge priority-${rec.priority}">${getPriorityText(rec.priority)}</div>
</div>
<div class="data-item">
<div class="data-label">📝 描述</div>
<div class="data-value">${rec.description}</div>
</div>
<div class="data-item">
<div class="data-label">📋 原因</div>
<div class="data-value">${rec.reason}</div>
</div>
${Object.keys(rec.parameters).length > 0 ? `
<div class="data-item">
<div class="data-label">⚙️ 参数</div>
<div class="data-value">${formatParameters(rec.parameters)}</div>
</div>
` : ''}
</div>
`).join('');
} else {
recommendationsData.innerHTML = '<div class="data-item"><div class="data-value">无设计建议</div></div>';
}
} else {
recommendationsData.innerHTML = '<div class="data-item"><div class="data-value">无建议数据</div></div>';
}
}
function displayMetricsData(task) {
if (task.analysis_result && task.analysis_result.quality_metrics) {
const metrics = task.analysis_result.quality_metrics;
metricsData.innerHTML = `
<div class="metric-item">
<div class="metric-label">体积利用率</div>
<div class="metric-value ${getMetricClass(metrics.volume_utilization, 0.3, 0.6)}">
${(metrics.volume_utilization * 100).toFixed(1)}%
</div>
<div class="data-value">${getVolumeUtilizationText(metrics.volume_utilization)}</div>
</div>
<div class="metric-item">
<div class="metric-label">拓扑复杂度</div>
<div class="metric-value ${getMetricClass(metrics.topology_complexity, 0.3, 0.7, true)}">
${metrics.topology_complexity.toFixed(2)}
</div>
<div class="data-value">${getComplexityText(metrics.topology_complexity)}</div>
</div>
<div class="metric-item">
<div class="metric-label">壁厚均匀性</div>
<div class="metric-value ${getMetricClass(metrics.wall_uniformity, 0.6, 0.8)}">
${(metrics.wall_uniformity * 100).toFixed(1)}%
</div>
<div class="data-value">${getUniformityText(metrics.wall_uniformity)}</div>
</div>
`;
} else {
metricsData.innerHTML = '<div class="data-item"><div class="data-value">无质量指标数据</div></div>';
}
}
function displayAnalysisInfo(task) {
let infoHTML = '';
if (task.geometry_data) {
const geo = task.geometry_data;
infoHTML += `
<div class="data-item">
<div class="data-label">🔧 分析方法</div>
<div class="data-value">${geo.analysis_method || '未知'}</div>
</div>
`;
}
if (task.analysis_result) {
const analysis = task.analysis_result;
infoHTML += `
<div class="data-item">
<div class="data-label">✅ 分析状态</div>
<div class="data-value">${task.status === 'completed' ? '分析完成' : '分析中'}</div>
</div>
<div class="data-item">
<div class="data-label">📋 分析摘要</div>
<div class="data-value">${analysis.analysis_summary || '无摘要'}</div>
</div>
`;
}
infoHTML += `
<div class="data-item">
<div class="data-label">📅 处理时间</div>
<div class="data-value">${task.completed_at ? new Date(task.completed_at).toLocaleString() : new Date().toLocaleString()}</div>
</div>
`;
analysisInfo.innerHTML = infoHTML;
}
// 工具函数
function getStatusText(status) {
const statusMap = {
'processing': '处理中',
'completed': '已完成',
'failed': '失败'
};
return statusMap[status] || status;
}
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatNumber(num) {
if (!num) return 'N/A';
if (num >= 1000000) {
return (num / 1000000).toFixed(2) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(2) + 'K';
} else {
return num.toFixed(2);
}
}
function calculateTopologyComplexity(topo) {
const totalElements = (topo.faces || 0) + (topo.edges || 0) + (topo.vertices || 0);
if (totalElements < 100) return '简单';
if (totalElements < 1000) return '中等';
return '复杂';
}
function getFeatureTypeText(type) {
const typeMap = {
'thin_wall': '薄壁区域',
'thick_wall': '厚壁区域',
'rib_structure': '加强筋结构',
'boss_feature': 'BOSS柱',
'draft_angle': '拔模角度',
'cooling_system': '冷却系统'
};
return typeMap[type] || type;
}
function getRecommendationTypeText(type) {
const typeMap = {
'wall_thickness': '壁厚优化',
'draft_angle': '拔模角度',
'rib_design': '加强筋设计',
'boss_design': 'BOSS柱设计',
'cooling_system': '冷却系统'
};
return typeMap[type] || type;
}
function getPriorityText(priority) {
const priorityMap = {
'high': '高优先级',
'medium': '中优先级',
'low': '低优先级'
};
return priorityMap[priority] || priority;
}
function formatParameters(parameters) {
return Object.entries(parameters).map(([key, value]) => {
if (typeof value === 'number') {
return `${key}: ${value.toFixed(2)}`;
}
return `${key}: ${value}`;
}).join('; ');
}
function getMetricClass(value, goodThreshold, excellentThreshold, reverse = false) {
if (reverse) {
if (value <= goodThreshold) return 'metric-good';
if (value <= excellentThreshold) return 'metric-warning';
return 'metric-poor';
} else {
if (value >= excellentThreshold) return 'metric-good';
if (value >= goodThreshold) return 'metric-warning';
return 'metric-poor';
}
}
function getVolumeUtilizationText(value) {
if (value >= 0.6) return '优秀';
if (value >= 0.3) return '良好';
return '待优化';
}
function getComplexityText(value) {
if (value <= 0.3) return '简单';
if (value <= 0.7) return '中等';
return '复杂';
}
function getUniformityText(value) {
if (value >= 0.8) return '均匀';
if (value >= 0.6) return '一般';
return '不均匀';
}
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
function hideError() {
errorMessage.style.display = 'none';
}
// 添加显示函数
function displayCavityData(cavityData) {
const cavityDiv = document.getElementById('cavityData');
cavityDiv.innerHTML = `
<pre>${JSON.stringify(cavityData, null, 2)}</pre>
`;
}
function displayKeyInfo(keyInfo) {
const keyInfoDiv = document.getElementById('keyInfoData');
if (!keyInfo) {
keyInfoDiv.innerHTML = '<div class="data-item"><div class="data-value">无关键信息数据</div></div>';
return;
}
const moldParams = keyInfo.mold_parameters || {};
const geoChars = keyInfo.geometric_characteristics || {};
const manuReqs = keyInfo.manufacturing_requirements || {};
keyInfoDiv.innerHTML = `
<h4>模具参数</h4>
<p>收缩率: ${moldParams.shrinkage_rate || 'N/A'}</p>
<p>拔模角: ${moldParams.draft_angle || 'N/A'}</p>
<p>分型线长度: ${moldParams.parting_line_length || 'N/A'} mm</p>
<h4>几何特性</h4>
<p>产品体积: ${geoChars.product_volume || 'N/A'}</p>
<p>产品重量: ${geoChars.product_weight || 'N/A'}</p>
<p>壁厚范围: ${geoChars.wall_thickness_range || 'N/A'}</p>
<h4>制造要求</h4>
<p>型腔材料: ${manuReqs.cavity_material || 'N/A'}</p>
<p>硬度: ${manuReqs.hardness || 'N/A'}</p>
<p>表面光洁度: ${manuReqs.surface_finish || 'N/A'}</p>
<p>预估周期: ${manuReqs.estimated_cycle_time || 'N/A'}</p>
`;
}
+460
View File
@@ -0,0 +1,460 @@
/* static/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2c3e50, #34495e);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.upload-section {
padding: 40px;
text-align: center;
}
.upload-area {
border: 3px dashed #3498db;
border-radius: 10px;
padding: 60px 40px;
margin: 20px 0;
background: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #2980b9;
background: #e8f4fc;
}
.upload-area.dragover {
border-color: #27ae60;
background: #d5f4e6;
}
.upload-icon {
font-size: 4em;
color: #3498db;
margin-bottom: 20px;
}
.file-input {
display: none;
}
.upload-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
padding: 15px 40px;
font-size: 1.1em;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
margin: 10px;
}
.upload-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(52, 152, 219, 0.3);
}
.upload-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.results-section {
padding: 0 40px 40px;
}
.result-card {
background: #f8f9fa;
border-radius: 10px;
padding: 25px;
margin: 15px 0;
border-left: 5px solid #3498db;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.result-card h3 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
font-size: 1.3em;
}
.task-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.geometry-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.bounding-box-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.topology-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.features-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.recommendations-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 15px;
}
.metrics-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.analysis-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.data-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.data-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.data-label {
font-weight: bold;
color: #2c3e50;
margin-bottom: 8px;
font-size: 0.95em;
display: flex;
align-items: center;
gap: 8px;
}
.data-value {
color: #34495e;
font-family: 'Courier New', monospace;
font-size: 1.1em;
font-weight: 600;
}
.data-unit {
color: #7f8c8d;
font-size: 0.9em;
margin-left: 4px;
}
.coordinate-item {
background: linear-gradient(135deg, #e8f4fc, #d1edff);
padding: 15px;
border-radius: 6px;
}
.coordinate-label {
font-weight: bold;
color: #2980b9;
margin-bottom: 5px;
}
.coordinate-value {
font-family: 'Courier New', monospace;
color: #2c3e50;
}
.status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
display: inline-block;
}
.status-processing {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.status-completed {
background: #d1edff;
color: #0c5460;
border: 1px solid #bee5eb;
}
.status-failed {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
display: none;
text-align: center;
padding: 30px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
display: none;
border-left: 5px solid #e74c3c;
}
.system-info {
background: #2c3e50;
color: white;
padding: 20px;
text-align: center;
margin-top: 20px;
border-radius: 0 0 15px 15px;
}
/* 特征和建议的特殊样式 */
.feature-item {
background: linear-gradient(135deg, #e8f4fc, #d1edff);
padding: 20px;
border-radius: 10px;
border-left: 4px solid #3498db;
}
.recommendation-item {
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
padding: 20px;
border-radius: 10px;
border-left: 4px solid #f39c12;
}
.recommendation-high {
border-left-color: #e74c3c;
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
}
.recommendation-medium {
border-left-color: #f39c12;
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
}
.recommendation-low {
border-left-color: #27ae60;
background: linear-gradient(135deg, #d1edff, #bee5eb);
}
.feature-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.feature-type {
font-weight: bold;
color: #2c3e50;
font-size: 1.1em;
}
.confidence-badge {
background: #3498db;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9em;
}
.recommendation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.priority-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9em;
font-weight: bold;
}
.priority-high {
background: #e74c3c;
color: white;
}
.priority-medium {
background: #f39c12;
color: white;
}
.priority-low {
background: #27ae60;
color: white;
}
.recommendation-list {
list-style: none;
padding: 0;
margin: 10px 0;
}
.recommendation-list li {
padding: 8px 0;
border-bottom: 1px solid #eee;
display: flex;
align-items: flex-start;
gap: 8px;
}
.recommendation-list li:before {
content: "💡";
font-size: 1.1em;
}
.recommendation-list li:last-child {
border-bottom: none;
}
.metric-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.metric-value {
font-size: 1.8em;
font-weight: bold;
color: #2c3e50;
margin: 10px 0;
}
.metric-label {
color: #7f8c8d;
font-size: 0.9em;
}
.metric-good {
color: #27ae60;
}
.metric-warning {
color: #f39c12;
}
.metric-poor {
color: #e74c3c;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 10px;
}
.header {
padding: 20px;
}
.header h1 {
font-size: 2em;
}
.upload-section {
padding: 20px;
}
.upload-area {
padding: 30px 20px;
}
.results-section {
padding: 0 20px 20px;
}
.geometry-data,
.bounding-box-data,
.topology-data,
.features-data,
.recommendations-data,
.metrics-data,
.analysis-info {
grid-template-columns: 1fr;
}
}
+123
View File
@@ -0,0 +1,123 @@
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>STP文件几何分析工具</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 STP文件几何分析工具</h1>
<p>上传STP/STEP文件,自动分析几何属性和拓扑结构</p>
</div>
<!-- 上传区域 - 默认显示 -->
<div class="upload-section" id="uploadSection">
<h2>选择STP文件</h2>
<p>支持 .stp 和 .step 格式文件</p>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<h3>拖放文件到此处或点击选择</h3>
<p>最大文件大小: 100MB</p>
<input type="file" id="fileInput" class="file-input" accept=".stp,.step">
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
选择文件
</button>
</div>
<button class="upload-btn" id="uploadBtn" onclick="uploadFile()" disabled>
开始分析
</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在分析文件,请稍候...</p>
</div>
<div class="error-message" id="errorMessage"></div>
</div>
<!-- 结果区域 - 默认隐藏 -->
<div class="results-section" id="resultsSection" style="display: none;">
<h2>📊 分析结果</h2>
<!-- 任务基本信息 -->
<div class="result-card">
<h3>📝 任务信息</h3>
<div class="task-info" id="taskInfo"></div>
</div>
<!-- 几何属性 -->
<div class="result-card">
<h3>📐 几何属性</h3>
<div class="geometry-data" id="geometryData"></div>
</div>
<!-- 边界框信息 -->
<div class="result-card">
<h3>📦 边界框信息</h3>
<div class="bounding-box-data" id="boundingBoxData"></div>
</div>
<!-- 拓扑结构 -->
<div class="result-card">
<h3>🔺 拓扑结构</h3>
<div class="topology-data" id="topologyData"></div>
</div>
<div class="result-card">
<h3>🔧 模具型腔设计</h3>
<div class="cavity-data" id="cavityData"></div>
</div>
<div class="result-card">
<h3>📐 关键工艺参数</h3>
<div class="key-info-data" id="keyInfoData"></div>
</div>
<!-- 检测到的特征 -->
<div class="result-card">
<h3>🎯 检测到的特征</h3>
<div class="features-data" id="featuresData"></div>
</div>
<!-- 设计建议 -->
<div class="result-card">
<h3>💡 设计建议</h3>
<div class="recommendations-data" id="recommendationsData"></div>
</div>
<!-- 质量指标 -->
<div class="result-card">
<h3>📈 质量指标</h3>
<div class="metrics-data" id="metricsData"></div>
</div>
<!-- 分析信息 -->
<div class="result-card">
<h3>🔍 分析信息</h3>
<div class="analysis-info" id="analysisInfo"></div>
</div>
<!-- 返回按钮 -->
<div style="text-align: center; margin-top: 30px;">
<button class="upload-btn" onclick="showUploadSection()">
🔄 分析新文件
</button>
</div>
</div>
<div class="system-info">
<p>PythonOCC 可用: {{ pythonocc_available }} | 服务版本: 2.0.0</p>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff