Compare commits
10 Commits
da4a9de4a3
...
e63d81f0aa
| Author | SHA1 | Date | |
|---|---|---|---|
| e63d81f0aa | |||
| 2c5d840117 | |||
| d95d8610b4 | |||
| f52006554c | |||
| aa1c91b818 | |||
| 09e586734b | |||
| c823f554a1 | |||
| 89660bba4e | |||
| 7b09eb3d89 | |||
| 96000f3f11 |
@@ -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
|
||||
@@ -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
@@ -0,0 +1,19 @@
|
||||
# Python 虚拟环境
|
||||
venv/
|
||||
env/
|
||||
.env
|
||||
.venv/
|
||||
|
||||
# Python 缓存文件
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# IDE 配置(可选)
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# 项目临时文件
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+170
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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` - 配置示例
|
||||
@@ -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}
|
||||
@@ -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())
|
||||
@@ -0,0 +1,2 @@
|
||||
# config package
|
||||
# 配置模块包
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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. **日志监控**:记录所有访问和操作
|
||||
@@ -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>
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
# API 模块
|
||||
@@ -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())
|
||||
@@ -0,0 +1 @@
|
||||
# Core 模块
|
||||
@@ -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 "分析完成"
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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}')>"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Services 模块
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
# storage/__init__.py
|
||||
from .rustfs_storage import RustFSManager, rustfs_manager
|
||||
|
||||
__all__ = ['RustFSManager', 'rustfs_manager']
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
# Utils 模块
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user