init
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# 服务配置
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
|
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,33 @@
|
|||||||
|
# 服务器配置
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
|
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
|
||||||
|
|
||||||
|
# PostgreSQL 数据库配置
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=moldinsight
|
||||||
|
DB_USER=moldinsight_user
|
||||||
|
DB_PASSWORD=your_secure_password_here
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
+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环境下正常运行!
|
||||||
@@ -1,2 +1,73 @@
|
|||||||
# 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. 访问 http://localhost:8000
|
||||||
@@ -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,91 @@
|
|||||||
|
# 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:
|
||||||
|
- "10001:8000" # 宿主机端口:容器端口
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./html_output:/app/html_output
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./.env:/app/.env:ro
|
||||||
|
environment:
|
||||||
|
# 应用内部使用8000端口(容器内)
|
||||||
|
- 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>
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
# 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
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 直接从环境变量获取端口,避免配置导入问题
|
||||||
|
host = os.getenv('HOST', '0.0.0.0')
|
||||||
|
port = int(os.getenv('PORT', '8000'))
|
||||||
|
|
||||||
|
print("启动模具几何分析服务 v3.0...")
|
||||||
|
print(f"访问 http://localhost:{port} 使用网页界面")
|
||||||
|
print("新增功能:")
|
||||||
|
print(" - STP文件解析为JSON数据")
|
||||||
|
print(" - 数据存储到PostgreSQL数据库")
|
||||||
|
print(" - 自动生成3D可视化HTML页面")
|
||||||
|
print(" - 源文件、JSON数据、HTML文件统一管理")
|
||||||
|
print(f"调试接口: http://localhost:{port}/debug/tasks")
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host=host,
|
||||||
|
port=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,65 @@
|
|||||||
|
#!/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 服务..."
|
||||||
|
echo "🌐 服务将在 http://localhost:8000 启动"
|
||||||
|
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,65 @@
|
|||||||
|
#!/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 服务..."
|
||||||
|
echo "🌐 服务将在 http://localhost:8000 启动"
|
||||||
|
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
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV=/opt/moldinsight/moldinsight_project/venv
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1='(venv) '"${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT='(venv) '
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV /opt/moldinsight/moldinsight_project/venv
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = '(venv) '"$prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/); you cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV /opt/moldinsight/moldinsight_project/venv
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dotenv.__main__ import cli
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from numpy.f2py.f2py2e import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fastapi.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fontTools.__main__ import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from charset_normalizer.cli import cli_detect
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli_detect())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from numpy._configtool import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fontTools.merge import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fontTools.subset import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/opt/anaconda3/envs/moldinsight/bin/python3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from scooby.__main__ import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from trimesh import __main__
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(__main__.main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fontTools.ttx import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/opt/moldinsight/moldinsight_project/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from uvicorn.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||||
|
|
||||||
|
/* Greenlet object interface */
|
||||||
|
|
||||||
|
#ifndef Py_GREENLETOBJECT_H
|
||||||
|
#define Py_GREENLETOBJECT_H
|
||||||
|
|
||||||
|
|
||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* This is deprecated and undocumented. It does not change. */
|
||||||
|
#define GREENLET_VERSION "1.0.0"
|
||||||
|
|
||||||
|
#ifndef GREENLET_MODULE
|
||||||
|
#define implementation_ptr_t void*
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct _greenlet {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject* weakreflist;
|
||||||
|
PyObject* dict;
|
||||||
|
implementation_ptr_t pimpl;
|
||||||
|
} PyGreenlet;
|
||||||
|
|
||||||
|
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||||
|
|
||||||
|
|
||||||
|
/* C API functions */
|
||||||
|
|
||||||
|
/* Total number of symbols that are exported */
|
||||||
|
#define PyGreenlet_API_pointers 12
|
||||||
|
|
||||||
|
#define PyGreenlet_Type_NUM 0
|
||||||
|
#define PyExc_GreenletError_NUM 1
|
||||||
|
#define PyExc_GreenletExit_NUM 2
|
||||||
|
|
||||||
|
#define PyGreenlet_New_NUM 3
|
||||||
|
#define PyGreenlet_GetCurrent_NUM 4
|
||||||
|
#define PyGreenlet_Throw_NUM 5
|
||||||
|
#define PyGreenlet_Switch_NUM 6
|
||||||
|
#define PyGreenlet_SetParent_NUM 7
|
||||||
|
|
||||||
|
#define PyGreenlet_MAIN_NUM 8
|
||||||
|
#define PyGreenlet_STARTED_NUM 9
|
||||||
|
#define PyGreenlet_ACTIVE_NUM 10
|
||||||
|
#define PyGreenlet_GET_PARENT_NUM 11
|
||||||
|
|
||||||
|
#ifndef GREENLET_MODULE
|
||||||
|
/* This section is used by modules that uses the greenlet C API */
|
||||||
|
static void** _PyGreenlet_API = NULL;
|
||||||
|
|
||||||
|
# define PyGreenlet_Type \
|
||||||
|
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||||
|
|
||||||
|
# define PyExc_GreenletError \
|
||||||
|
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||||
|
|
||||||
|
# define PyExc_GreenletExit \
|
||||||
|
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_New(PyObject *args)
|
||||||
|
*
|
||||||
|
* greenlet.greenlet(run, parent=None)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_New \
|
||||||
|
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_GetCurrent(void)
|
||||||
|
*
|
||||||
|
* greenlet.getcurrent()
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GetCurrent \
|
||||||
|
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_Throw(
|
||||||
|
* PyGreenlet *greenlet,
|
||||||
|
* PyObject *typ,
|
||||||
|
* PyObject *val,
|
||||||
|
* PyObject *tb)
|
||||||
|
*
|
||||||
|
* g.throw(...)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_Throw \
|
||||||
|
(*(PyObject * (*)(PyGreenlet * self, \
|
||||||
|
PyObject * typ, \
|
||||||
|
PyObject * val, \
|
||||||
|
PyObject * tb)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||||
|
*
|
||||||
|
* g.switch(*args, **kwargs)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_Switch \
|
||||||
|
(*(PyObject * \
|
||||||
|
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||||
|
*
|
||||||
|
* g.parent = new_parent
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_SetParent \
|
||||||
|
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||||
|
*
|
||||||
|
* return greenlet.parent;
|
||||||
|
*
|
||||||
|
* This could return NULL even if there is no exception active.
|
||||||
|
* If it does not return NULL, you are responsible for decrementing the
|
||||||
|
* reference count.
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GetParent \
|
||||||
|
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* deprecated, undocumented alias.
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||||
|
|
||||||
|
# define PyGreenlet_MAIN \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||||
|
|
||||||
|
# define PyGreenlet_STARTED \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||||
|
|
||||||
|
# define PyGreenlet_ACTIVE \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Macro that imports greenlet and initializes C API */
|
||||||
|
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||||
|
keep the older definition to be sure older code that might have a copy of
|
||||||
|
the header still works. */
|
||||||
|
# define PyGreenlet_Import() \
|
||||||
|
{ \
|
||||||
|
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* GREENLET_MODULE */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* !Py_GREENLETOBJECT_H */
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# bitmap distribution font (bdf) file parser
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 1996-05-16 fl created (as bdf2pil)
|
||||||
|
# 1997-08-25 fl converted to FontFile driver
|
||||||
|
# 2001-05-25 fl removed bogus __init__ call
|
||||||
|
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
|
||||||
|
# 2003-04-22 fl more robustification (from Graham Dumpleton)
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2003 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1997-2003 by Fredrik Lundh.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parse X Bitmap Distribution Format (BDF)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import FontFile, Image
|
||||||
|
|
||||||
|
|
||||||
|
def bdf_char(
|
||||||
|
f: BinaryIO,
|
||||||
|
) -> (
|
||||||
|
tuple[
|
||||||
|
str,
|
||||||
|
int,
|
||||||
|
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
):
|
||||||
|
# skip to STARTCHAR
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
if s.startswith(b"STARTCHAR"):
|
||||||
|
break
|
||||||
|
id = s[9:].strip().decode("ascii")
|
||||||
|
|
||||||
|
# load symbol properties
|
||||||
|
props = {}
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s or s.startswith(b"BITMAP"):
|
||||||
|
break
|
||||||
|
i = s.find(b" ")
|
||||||
|
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||||
|
|
||||||
|
# load bitmap
|
||||||
|
bitmap = bytearray()
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s or s.startswith(b"ENDCHAR"):
|
||||||
|
break
|
||||||
|
bitmap += s[:-1]
|
||||||
|
|
||||||
|
# The word BBX
|
||||||
|
# followed by the width in x (BBw), height in y (BBh),
|
||||||
|
# and x and y displacement (BBxoff0, BByoff0)
|
||||||
|
# of the lower left corner from the origin of the character.
|
||||||
|
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
||||||
|
|
||||||
|
# The word DWIDTH
|
||||||
|
# followed by the width in x and y of the character in device pixels.
|
||||||
|
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
||||||
|
|
||||||
|
bbox = (
|
||||||
|
(dwx, dwy),
|
||||||
|
(x_disp, -y_disp - height, width + x_disp, -y_disp),
|
||||||
|
(0, 0, width, height),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
|
||||||
|
except ValueError:
|
||||||
|
# deal with zero-width characters
|
||||||
|
im = Image.new("1", (width, height))
|
||||||
|
|
||||||
|
return id, int(props["ENCODING"]), bbox, im
|
||||||
|
|
||||||
|
|
||||||
|
class BdfFontFile(FontFile.FontFile):
|
||||||
|
"""Font file plugin for the X11 BDF format."""
|
||||||
|
|
||||||
|
def __init__(self, fp: BinaryIO) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
s = fp.readline()
|
||||||
|
if not s.startswith(b"STARTFONT 2.1"):
|
||||||
|
msg = "not a valid BDF file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
props = {}
|
||||||
|
comments = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
s = fp.readline()
|
||||||
|
if not s or s.startswith(b"ENDPROPERTIES"):
|
||||||
|
break
|
||||||
|
i = s.find(b" ")
|
||||||
|
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||||
|
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
|
||||||
|
if s.find(b"LogicalFontDescription") < 0:
|
||||||
|
comments.append(s[i + 1 : -1].decode("ascii"))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
c = bdf_char(fp)
|
||||||
|
if not c:
|
||||||
|
break
|
||||||
|
id, ch, (xy, dst, src), im = c
|
||||||
|
if 0 <= ch < len(self.glyph):
|
||||||
|
self.glyph[ch] = xy, dst, src, im
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# a class to read from a container file
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1995-06-18 fl Created
|
||||||
|
# 1995-09-07 fl Added readline(), readlines()
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2001 by Secret Labs AB
|
||||||
|
# Copyright (c) 1995 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import IO, AnyStr, NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerIO(IO[AnyStr]):
|
||||||
|
"""
|
||||||
|
A file object that provides read access to a part of an existing
|
||||||
|
file (for example a TAR file).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
|
||||||
|
"""
|
||||||
|
Create file object.
|
||||||
|
|
||||||
|
:param file: Existing file.
|
||||||
|
:param offset: Start of region, in bytes.
|
||||||
|
:param length: Size of region, in bytes.
|
||||||
|
"""
|
||||||
|
self.fh: IO[AnyStr] = file
|
||||||
|
self.pos = 0
|
||||||
|
self.offset = offset
|
||||||
|
self.length = length
|
||||||
|
self.fh.seek(offset)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Always false.
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def seekable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seek(self, offset: int, mode: int = io.SEEK_SET) -> int:
|
||||||
|
"""
|
||||||
|
Move file pointer.
|
||||||
|
|
||||||
|
:param offset: Offset in bytes.
|
||||||
|
:param mode: Starting position. Use 0 for beginning of region, 1
|
||||||
|
for current offset, and 2 for end of region. You cannot move
|
||||||
|
the pointer outside the defined region.
|
||||||
|
:returns: Offset from start of region, in bytes.
|
||||||
|
"""
|
||||||
|
if mode == 1:
|
||||||
|
self.pos = self.pos + offset
|
||||||
|
elif mode == 2:
|
||||||
|
self.pos = self.length + offset
|
||||||
|
else:
|
||||||
|
self.pos = offset
|
||||||
|
# clamp
|
||||||
|
self.pos = max(0, min(self.pos, self.length))
|
||||||
|
self.fh.seek(self.offset + self.pos)
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
"""
|
||||||
|
Get current file pointer.
|
||||||
|
|
||||||
|
:returns: Offset from start of region, in bytes.
|
||||||
|
"""
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def readable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def read(self, n: int = -1) -> AnyStr:
|
||||||
|
"""
|
||||||
|
Read data.
|
||||||
|
|
||||||
|
:param n: Number of bytes to read. If omitted, zero or negative,
|
||||||
|
read until end of region.
|
||||||
|
:returns: An 8-bit string.
|
||||||
|
"""
|
||||||
|
if n > 0:
|
||||||
|
n = min(n, self.length - self.pos)
|
||||||
|
else:
|
||||||
|
n = self.length - self.pos
|
||||||
|
if n <= 0: # EOF
|
||||||
|
return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
|
||||||
|
self.pos = self.pos + n
|
||||||
|
return self.fh.read(n)
|
||||||
|
|
||||||
|
def readline(self, n: int = -1) -> AnyStr:
|
||||||
|
"""
|
||||||
|
Read a line of text.
|
||||||
|
|
||||||
|
:param n: Number of bytes to read. If omitted, zero or negative,
|
||||||
|
read until end of line.
|
||||||
|
:returns: An 8-bit string.
|
||||||
|
"""
|
||||||
|
s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
|
||||||
|
newline_character = b"\n" if "b" in self.fh.mode else "\n"
|
||||||
|
while True:
|
||||||
|
c = self.read(1)
|
||||||
|
if not c:
|
||||||
|
break
|
||||||
|
s = s + c
|
||||||
|
if c == newline_character or len(s) == n:
|
||||||
|
break
|
||||||
|
return s
|
||||||
|
|
||||||
|
def readlines(self, n: int | None = -1) -> list[AnyStr]:
|
||||||
|
"""
|
||||||
|
Read multiple lines of text.
|
||||||
|
|
||||||
|
:param n: Number of lines to read. If omitted, zero, negative or None,
|
||||||
|
read until end of region.
|
||||||
|
:returns: A list of 8-bit strings.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
while True:
|
||||||
|
s = self.readline()
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
lines.append(s)
|
||||||
|
if len(lines) == n:
|
||||||
|
break
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def writable(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write(self, b: AnyStr) -> NoReturn:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def writelines(self, lines: Iterable[AnyStr]) -> NoReturn:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def truncate(self, size: int | None = None) -> int:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __enter__(self) -> ContainerIO[AnyStr]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args: object) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __iter__(self) -> ContainerIO[AnyStr]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> AnyStr:
|
||||||
|
line = self.readline()
|
||||||
|
if not line:
|
||||||
|
msg = "end of region"
|
||||||
|
raise StopIteration(msg)
|
||||||
|
return line
|
||||||
|
|
||||||
|
def fileno(self) -> int:
|
||||||
|
return self.fh.fileno()
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
self.fh.flush()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.fh.close()
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# EXIF tags
|
||||||
|
#
|
||||||
|
# Copyright (c) 2003 by Secret Labs AB
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module provides constants and clear-text names for various
|
||||||
|
well-known EXIF tags.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Base(IntEnum):
|
||||||
|
# possibly incomplete
|
||||||
|
InteropIndex = 0x0001
|
||||||
|
ProcessingSoftware = 0x000B
|
||||||
|
NewSubfileType = 0x00FE
|
||||||
|
SubfileType = 0x00FF
|
||||||
|
ImageWidth = 0x0100
|
||||||
|
ImageLength = 0x0101
|
||||||
|
BitsPerSample = 0x0102
|
||||||
|
Compression = 0x0103
|
||||||
|
PhotometricInterpretation = 0x0106
|
||||||
|
Thresholding = 0x0107
|
||||||
|
CellWidth = 0x0108
|
||||||
|
CellLength = 0x0109
|
||||||
|
FillOrder = 0x010A
|
||||||
|
DocumentName = 0x010D
|
||||||
|
ImageDescription = 0x010E
|
||||||
|
Make = 0x010F
|
||||||
|
Model = 0x0110
|
||||||
|
StripOffsets = 0x0111
|
||||||
|
Orientation = 0x0112
|
||||||
|
SamplesPerPixel = 0x0115
|
||||||
|
RowsPerStrip = 0x0116
|
||||||
|
StripByteCounts = 0x0117
|
||||||
|
MinSampleValue = 0x0118
|
||||||
|
MaxSampleValue = 0x0119
|
||||||
|
XResolution = 0x011A
|
||||||
|
YResolution = 0x011B
|
||||||
|
PlanarConfiguration = 0x011C
|
||||||
|
PageName = 0x011D
|
||||||
|
FreeOffsets = 0x0120
|
||||||
|
FreeByteCounts = 0x0121
|
||||||
|
GrayResponseUnit = 0x0122
|
||||||
|
GrayResponseCurve = 0x0123
|
||||||
|
T4Options = 0x0124
|
||||||
|
T6Options = 0x0125
|
||||||
|
ResolutionUnit = 0x0128
|
||||||
|
PageNumber = 0x0129
|
||||||
|
TransferFunction = 0x012D
|
||||||
|
Software = 0x0131
|
||||||
|
DateTime = 0x0132
|
||||||
|
Artist = 0x013B
|
||||||
|
HostComputer = 0x013C
|
||||||
|
Predictor = 0x013D
|
||||||
|
WhitePoint = 0x013E
|
||||||
|
PrimaryChromaticities = 0x013F
|
||||||
|
ColorMap = 0x0140
|
||||||
|
HalftoneHints = 0x0141
|
||||||
|
TileWidth = 0x0142
|
||||||
|
TileLength = 0x0143
|
||||||
|
TileOffsets = 0x0144
|
||||||
|
TileByteCounts = 0x0145
|
||||||
|
SubIFDs = 0x014A
|
||||||
|
InkSet = 0x014C
|
||||||
|
InkNames = 0x014D
|
||||||
|
NumberOfInks = 0x014E
|
||||||
|
DotRange = 0x0150
|
||||||
|
TargetPrinter = 0x0151
|
||||||
|
ExtraSamples = 0x0152
|
||||||
|
SampleFormat = 0x0153
|
||||||
|
SMinSampleValue = 0x0154
|
||||||
|
SMaxSampleValue = 0x0155
|
||||||
|
TransferRange = 0x0156
|
||||||
|
ClipPath = 0x0157
|
||||||
|
XClipPathUnits = 0x0158
|
||||||
|
YClipPathUnits = 0x0159
|
||||||
|
Indexed = 0x015A
|
||||||
|
JPEGTables = 0x015B
|
||||||
|
OPIProxy = 0x015F
|
||||||
|
JPEGProc = 0x0200
|
||||||
|
JpegIFOffset = 0x0201
|
||||||
|
JpegIFByteCount = 0x0202
|
||||||
|
JpegRestartInterval = 0x0203
|
||||||
|
JpegLosslessPredictors = 0x0205
|
||||||
|
JpegPointTransforms = 0x0206
|
||||||
|
JpegQTables = 0x0207
|
||||||
|
JpegDCTables = 0x0208
|
||||||
|
JpegACTables = 0x0209
|
||||||
|
YCbCrCoefficients = 0x0211
|
||||||
|
YCbCrSubSampling = 0x0212
|
||||||
|
YCbCrPositioning = 0x0213
|
||||||
|
ReferenceBlackWhite = 0x0214
|
||||||
|
XMLPacket = 0x02BC
|
||||||
|
RelatedImageFileFormat = 0x1000
|
||||||
|
RelatedImageWidth = 0x1001
|
||||||
|
RelatedImageLength = 0x1002
|
||||||
|
Rating = 0x4746
|
||||||
|
RatingPercent = 0x4749
|
||||||
|
ImageID = 0x800D
|
||||||
|
CFARepeatPatternDim = 0x828D
|
||||||
|
BatteryLevel = 0x828F
|
||||||
|
Copyright = 0x8298
|
||||||
|
ExposureTime = 0x829A
|
||||||
|
FNumber = 0x829D
|
||||||
|
IPTCNAA = 0x83BB
|
||||||
|
ImageResources = 0x8649
|
||||||
|
ExifOffset = 0x8769
|
||||||
|
InterColorProfile = 0x8773
|
||||||
|
ExposureProgram = 0x8822
|
||||||
|
SpectralSensitivity = 0x8824
|
||||||
|
GPSInfo = 0x8825
|
||||||
|
ISOSpeedRatings = 0x8827
|
||||||
|
OECF = 0x8828
|
||||||
|
Interlace = 0x8829
|
||||||
|
TimeZoneOffset = 0x882A
|
||||||
|
SelfTimerMode = 0x882B
|
||||||
|
SensitivityType = 0x8830
|
||||||
|
StandardOutputSensitivity = 0x8831
|
||||||
|
RecommendedExposureIndex = 0x8832
|
||||||
|
ISOSpeed = 0x8833
|
||||||
|
ISOSpeedLatitudeyyy = 0x8834
|
||||||
|
ISOSpeedLatitudezzz = 0x8835
|
||||||
|
ExifVersion = 0x9000
|
||||||
|
DateTimeOriginal = 0x9003
|
||||||
|
DateTimeDigitized = 0x9004
|
||||||
|
OffsetTime = 0x9010
|
||||||
|
OffsetTimeOriginal = 0x9011
|
||||||
|
OffsetTimeDigitized = 0x9012
|
||||||
|
ComponentsConfiguration = 0x9101
|
||||||
|
CompressedBitsPerPixel = 0x9102
|
||||||
|
ShutterSpeedValue = 0x9201
|
||||||
|
ApertureValue = 0x9202
|
||||||
|
BrightnessValue = 0x9203
|
||||||
|
ExposureBiasValue = 0x9204
|
||||||
|
MaxApertureValue = 0x9205
|
||||||
|
SubjectDistance = 0x9206
|
||||||
|
MeteringMode = 0x9207
|
||||||
|
LightSource = 0x9208
|
||||||
|
Flash = 0x9209
|
||||||
|
FocalLength = 0x920A
|
||||||
|
Noise = 0x920D
|
||||||
|
ImageNumber = 0x9211
|
||||||
|
SecurityClassification = 0x9212
|
||||||
|
ImageHistory = 0x9213
|
||||||
|
TIFFEPStandardID = 0x9216
|
||||||
|
MakerNote = 0x927C
|
||||||
|
UserComment = 0x9286
|
||||||
|
SubsecTime = 0x9290
|
||||||
|
SubsecTimeOriginal = 0x9291
|
||||||
|
SubsecTimeDigitized = 0x9292
|
||||||
|
AmbientTemperature = 0x9400
|
||||||
|
Humidity = 0x9401
|
||||||
|
Pressure = 0x9402
|
||||||
|
WaterDepth = 0x9403
|
||||||
|
Acceleration = 0x9404
|
||||||
|
CameraElevationAngle = 0x9405
|
||||||
|
XPTitle = 0x9C9B
|
||||||
|
XPComment = 0x9C9C
|
||||||
|
XPAuthor = 0x9C9D
|
||||||
|
XPKeywords = 0x9C9E
|
||||||
|
XPSubject = 0x9C9F
|
||||||
|
FlashPixVersion = 0xA000
|
||||||
|
ColorSpace = 0xA001
|
||||||
|
ExifImageWidth = 0xA002
|
||||||
|
ExifImageHeight = 0xA003
|
||||||
|
RelatedSoundFile = 0xA004
|
||||||
|
ExifInteroperabilityOffset = 0xA005
|
||||||
|
FlashEnergy = 0xA20B
|
||||||
|
SpatialFrequencyResponse = 0xA20C
|
||||||
|
FocalPlaneXResolution = 0xA20E
|
||||||
|
FocalPlaneYResolution = 0xA20F
|
||||||
|
FocalPlaneResolutionUnit = 0xA210
|
||||||
|
SubjectLocation = 0xA214
|
||||||
|
ExposureIndex = 0xA215
|
||||||
|
SensingMethod = 0xA217
|
||||||
|
FileSource = 0xA300
|
||||||
|
SceneType = 0xA301
|
||||||
|
CFAPattern = 0xA302
|
||||||
|
CustomRendered = 0xA401
|
||||||
|
ExposureMode = 0xA402
|
||||||
|
WhiteBalance = 0xA403
|
||||||
|
DigitalZoomRatio = 0xA404
|
||||||
|
FocalLengthIn35mmFilm = 0xA405
|
||||||
|
SceneCaptureType = 0xA406
|
||||||
|
GainControl = 0xA407
|
||||||
|
Contrast = 0xA408
|
||||||
|
Saturation = 0xA409
|
||||||
|
Sharpness = 0xA40A
|
||||||
|
DeviceSettingDescription = 0xA40B
|
||||||
|
SubjectDistanceRange = 0xA40C
|
||||||
|
ImageUniqueID = 0xA420
|
||||||
|
CameraOwnerName = 0xA430
|
||||||
|
BodySerialNumber = 0xA431
|
||||||
|
LensSpecification = 0xA432
|
||||||
|
LensMake = 0xA433
|
||||||
|
LensModel = 0xA434
|
||||||
|
LensSerialNumber = 0xA435
|
||||||
|
CompositeImage = 0xA460
|
||||||
|
CompositeImageCount = 0xA461
|
||||||
|
CompositeImageExposureTimes = 0xA462
|
||||||
|
Gamma = 0xA500
|
||||||
|
PrintImageMatching = 0xC4A5
|
||||||
|
DNGVersion = 0xC612
|
||||||
|
DNGBackwardVersion = 0xC613
|
||||||
|
UniqueCameraModel = 0xC614
|
||||||
|
LocalizedCameraModel = 0xC615
|
||||||
|
CFAPlaneColor = 0xC616
|
||||||
|
CFALayout = 0xC617
|
||||||
|
LinearizationTable = 0xC618
|
||||||
|
BlackLevelRepeatDim = 0xC619
|
||||||
|
BlackLevel = 0xC61A
|
||||||
|
BlackLevelDeltaH = 0xC61B
|
||||||
|
BlackLevelDeltaV = 0xC61C
|
||||||
|
WhiteLevel = 0xC61D
|
||||||
|
DefaultScale = 0xC61E
|
||||||
|
DefaultCropOrigin = 0xC61F
|
||||||
|
DefaultCropSize = 0xC620
|
||||||
|
ColorMatrix1 = 0xC621
|
||||||
|
ColorMatrix2 = 0xC622
|
||||||
|
CameraCalibration1 = 0xC623
|
||||||
|
CameraCalibration2 = 0xC624
|
||||||
|
ReductionMatrix1 = 0xC625
|
||||||
|
ReductionMatrix2 = 0xC626
|
||||||
|
AnalogBalance = 0xC627
|
||||||
|
AsShotNeutral = 0xC628
|
||||||
|
AsShotWhiteXY = 0xC629
|
||||||
|
BaselineExposure = 0xC62A
|
||||||
|
BaselineNoise = 0xC62B
|
||||||
|
BaselineSharpness = 0xC62C
|
||||||
|
BayerGreenSplit = 0xC62D
|
||||||
|
LinearResponseLimit = 0xC62E
|
||||||
|
CameraSerialNumber = 0xC62F
|
||||||
|
LensInfo = 0xC630
|
||||||
|
ChromaBlurRadius = 0xC631
|
||||||
|
AntiAliasStrength = 0xC632
|
||||||
|
ShadowScale = 0xC633
|
||||||
|
DNGPrivateData = 0xC634
|
||||||
|
MakerNoteSafety = 0xC635
|
||||||
|
CalibrationIlluminant1 = 0xC65A
|
||||||
|
CalibrationIlluminant2 = 0xC65B
|
||||||
|
BestQualityScale = 0xC65C
|
||||||
|
RawDataUniqueID = 0xC65D
|
||||||
|
OriginalRawFileName = 0xC68B
|
||||||
|
OriginalRawFileData = 0xC68C
|
||||||
|
ActiveArea = 0xC68D
|
||||||
|
MaskedAreas = 0xC68E
|
||||||
|
AsShotICCProfile = 0xC68F
|
||||||
|
AsShotPreProfileMatrix = 0xC690
|
||||||
|
CurrentICCProfile = 0xC691
|
||||||
|
CurrentPreProfileMatrix = 0xC692
|
||||||
|
ColorimetricReference = 0xC6BF
|
||||||
|
CameraCalibrationSignature = 0xC6F3
|
||||||
|
ProfileCalibrationSignature = 0xC6F4
|
||||||
|
AsShotProfileName = 0xC6F6
|
||||||
|
NoiseReductionApplied = 0xC6F7
|
||||||
|
ProfileName = 0xC6F8
|
||||||
|
ProfileHueSatMapDims = 0xC6F9
|
||||||
|
ProfileHueSatMapData1 = 0xC6FA
|
||||||
|
ProfileHueSatMapData2 = 0xC6FB
|
||||||
|
ProfileToneCurve = 0xC6FC
|
||||||
|
ProfileEmbedPolicy = 0xC6FD
|
||||||
|
ProfileCopyright = 0xC6FE
|
||||||
|
ForwardMatrix1 = 0xC714
|
||||||
|
ForwardMatrix2 = 0xC715
|
||||||
|
PreviewApplicationName = 0xC716
|
||||||
|
PreviewApplicationVersion = 0xC717
|
||||||
|
PreviewSettingsName = 0xC718
|
||||||
|
PreviewSettingsDigest = 0xC719
|
||||||
|
PreviewColorSpace = 0xC71A
|
||||||
|
PreviewDateTime = 0xC71B
|
||||||
|
RawImageDigest = 0xC71C
|
||||||
|
OriginalRawFileDigest = 0xC71D
|
||||||
|
SubTileBlockSize = 0xC71E
|
||||||
|
RowInterleaveFactor = 0xC71F
|
||||||
|
ProfileLookTableDims = 0xC725
|
||||||
|
ProfileLookTableData = 0xC726
|
||||||
|
OpcodeList1 = 0xC740
|
||||||
|
OpcodeList2 = 0xC741
|
||||||
|
OpcodeList3 = 0xC74E
|
||||||
|
NoiseProfile = 0xC761
|
||||||
|
|
||||||
|
|
||||||
|
"""Maps EXIF tags to tag names."""
|
||||||
|
TAGS = {
|
||||||
|
**{i.value: i.name for i in Base},
|
||||||
|
0x920C: "SpatialFrequencyResponse",
|
||||||
|
0x9214: "SubjectLocation",
|
||||||
|
0x9215: "ExposureIndex",
|
||||||
|
0x828E: "CFAPattern",
|
||||||
|
0x920B: "FlashEnergy",
|
||||||
|
0x9216: "TIFF/EPStandardID",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GPS(IntEnum):
|
||||||
|
GPSVersionID = 0x00
|
||||||
|
GPSLatitudeRef = 0x01
|
||||||
|
GPSLatitude = 0x02
|
||||||
|
GPSLongitudeRef = 0x03
|
||||||
|
GPSLongitude = 0x04
|
||||||
|
GPSAltitudeRef = 0x05
|
||||||
|
GPSAltitude = 0x06
|
||||||
|
GPSTimeStamp = 0x07
|
||||||
|
GPSSatellites = 0x08
|
||||||
|
GPSStatus = 0x09
|
||||||
|
GPSMeasureMode = 0x0A
|
||||||
|
GPSDOP = 0x0B
|
||||||
|
GPSSpeedRef = 0x0C
|
||||||
|
GPSSpeed = 0x0D
|
||||||
|
GPSTrackRef = 0x0E
|
||||||
|
GPSTrack = 0x0F
|
||||||
|
GPSImgDirectionRef = 0x10
|
||||||
|
GPSImgDirection = 0x11
|
||||||
|
GPSMapDatum = 0x12
|
||||||
|
GPSDestLatitudeRef = 0x13
|
||||||
|
GPSDestLatitude = 0x14
|
||||||
|
GPSDestLongitudeRef = 0x15
|
||||||
|
GPSDestLongitude = 0x16
|
||||||
|
GPSDestBearingRef = 0x17
|
||||||
|
GPSDestBearing = 0x18
|
||||||
|
GPSDestDistanceRef = 0x19
|
||||||
|
GPSDestDistance = 0x1A
|
||||||
|
GPSProcessingMethod = 0x1B
|
||||||
|
GPSAreaInformation = 0x1C
|
||||||
|
GPSDateStamp = 0x1D
|
||||||
|
GPSDifferential = 0x1E
|
||||||
|
GPSHPositioningError = 0x1F
|
||||||
|
|
||||||
|
|
||||||
|
"""Maps EXIF GPS tags to tag names."""
|
||||||
|
GPSTAGS = {i.value: i.name for i in GPS}
|
||||||
|
|
||||||
|
|
||||||
|
class Interop(IntEnum):
|
||||||
|
InteropIndex = 0x0001
|
||||||
|
InteropVersion = 0x0002
|
||||||
|
RelatedImageFileFormat = 0x1000
|
||||||
|
RelatedImageWidth = 0x1001
|
||||||
|
RelatedImageHeight = 0x1002
|
||||||
|
|
||||||
|
|
||||||
|
class IFD(IntEnum):
|
||||||
|
Exif = 0x8769
|
||||||
|
GPSInfo = 0x8825
|
||||||
|
MakerNote = 0x927C
|
||||||
|
Makernote = 0x927C # Deprecated
|
||||||
|
Interop = 0xA005
|
||||||
|
IFD1 = -1
|
||||||
|
|
||||||
|
|
||||||
|
class LightSource(IntEnum):
|
||||||
|
Unknown = 0x00
|
||||||
|
Daylight = 0x01
|
||||||
|
Fluorescent = 0x02
|
||||||
|
Tungsten = 0x03
|
||||||
|
Flash = 0x04
|
||||||
|
Fine = 0x09
|
||||||
|
Cloudy = 0x0A
|
||||||
|
Shade = 0x0B
|
||||||
|
DaylightFluorescent = 0x0C
|
||||||
|
DayWhiteFluorescent = 0x0D
|
||||||
|
CoolWhiteFluorescent = 0x0E
|
||||||
|
WhiteFluorescent = 0x0F
|
||||||
|
StandardLightA = 0x11
|
||||||
|
StandardLightB = 0x12
|
||||||
|
StandardLightC = 0x13
|
||||||
|
D55 = 0x14
|
||||||
|
D65 = 0x15
|
||||||
|
D75 = 0x16
|
||||||
|
D50 = 0x17
|
||||||
|
ISO = 0x18
|
||||||
|
Other = 0xFF
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# base class for raster font file parsers
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 1997-06-05 fl created
|
||||||
|
# 1997-08-19 fl restrict image width
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-1998 by Secret Labs AB
|
||||||
|
# Copyright (c) 1997-1998 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import Image, _binary
|
||||||
|
|
||||||
|
WIDTH = 800
|
||||||
|
|
||||||
|
|
||||||
|
def puti16(
|
||||||
|
fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Write network order (big-endian) 16-bit sequence"""
|
||||||
|
for v in values:
|
||||||
|
if v < 0:
|
||||||
|
v += 65536
|
||||||
|
fp.write(_binary.o16be(v))
|
||||||
|
|
||||||
|
|
||||||
|
class FontFile:
|
||||||
|
"""Base class for raster font file handlers."""
|
||||||
|
|
||||||
|
bitmap: Image.Image | None = None
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.info: dict[bytes, bytes | int] = {}
|
||||||
|
self.glyph: list[
|
||||||
|
tuple[
|
||||||
|
tuple[int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
] = [None] * 256
|
||||||
|
|
||||||
|
def __getitem__(self, ix: int) -> (
|
||||||
|
tuple[
|
||||||
|
tuple[int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
):
|
||||||
|
return self.glyph[ix]
|
||||||
|
|
||||||
|
def compile(self) -> None:
|
||||||
|
"""Create metrics and bitmap"""
|
||||||
|
|
||||||
|
if self.bitmap:
|
||||||
|
return
|
||||||
|
|
||||||
|
# create bitmap large enough to hold all data
|
||||||
|
h = w = maxwidth = 0
|
||||||
|
lines = 1
|
||||||
|
for glyph in self.glyph:
|
||||||
|
if glyph:
|
||||||
|
d, dst, src, im = glyph
|
||||||
|
h = max(h, src[3] - src[1])
|
||||||
|
w = w + (src[2] - src[0])
|
||||||
|
if w > WIDTH:
|
||||||
|
lines += 1
|
||||||
|
w = src[2] - src[0]
|
||||||
|
maxwidth = max(maxwidth, w)
|
||||||
|
|
||||||
|
xsize = maxwidth
|
||||||
|
ysize = lines * h
|
||||||
|
|
||||||
|
if xsize == 0 and ysize == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ysize = h
|
||||||
|
|
||||||
|
# paste glyphs into bitmap
|
||||||
|
self.bitmap = Image.new("1", (xsize, ysize))
|
||||||
|
self.metrics: list[
|
||||||
|
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
|
||||||
|
| None
|
||||||
|
] = [None] * 256
|
||||||
|
x = y = 0
|
||||||
|
for i in range(256):
|
||||||
|
glyph = self[i]
|
||||||
|
if glyph:
|
||||||
|
d, dst, src, im = glyph
|
||||||
|
xx = src[2] - src[0]
|
||||||
|
x0, y0 = x, y
|
||||||
|
x = x + xx
|
||||||
|
if x > WIDTH:
|
||||||
|
x, y = 0, y + h
|
||||||
|
x0, y0 = x, y
|
||||||
|
x = xx
|
||||||
|
s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
|
||||||
|
self.bitmap.paste(im.crop(src), s)
|
||||||
|
self.metrics[i] = d, dst, s
|
||||||
|
|
||||||
|
def save(self, filename: str) -> None:
|
||||||
|
"""Save font"""
|
||||||
|
|
||||||
|
self.compile()
|
||||||
|
|
||||||
|
# font data
|
||||||
|
if not self.bitmap:
|
||||||
|
msg = "No bitmap created"
|
||||||
|
raise ValueError(msg)
|
||||||
|
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
|
||||||
|
|
||||||
|
# font metrics
|
||||||
|
with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
|
||||||
|
fp.write(b"PILfont\n")
|
||||||
|
fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
|
||||||
|
fp.write(b"DATA\n")
|
||||||
|
for id in range(256):
|
||||||
|
m = self.metrics[id]
|
||||||
|
if not m:
|
||||||
|
puti16(fp, (0,) * 10)
|
||||||
|
else:
|
||||||
|
puti16(fp, m[0] + m[1] + m[2])
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# GD file handling
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1996-04-12 fl Created
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1996 by Fredrik Lundh.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
.. note::
|
||||||
|
This format cannot be automatically recognized, so the
|
||||||
|
class is not registered for use with :py:func:`PIL.Image.open()`. To open a
|
||||||
|
gd file, use the :py:func:`PIL.GdImageFile.open()` function instead.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
|
||||||
|
implementation is provided for convenience and demonstrational
|
||||||
|
purposes only.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import ImageFile, ImagePalette, UnidentifiedImageError
|
||||||
|
from ._binary import i16be as i16
|
||||||
|
from ._binary import i32be as i32
|
||||||
|
from ._typing import StrOrBytesPath
|
||||||
|
|
||||||
|
|
||||||
|
class GdImageFile(ImageFile.ImageFile):
|
||||||
|
"""
|
||||||
|
Image plugin for the GD uncompressed format. Note that this format
|
||||||
|
is not supported by the standard :py:func:`PIL.Image.open()` function. To use
|
||||||
|
this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and
|
||||||
|
use the :py:func:`PIL.GdImageFile.open()` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
format = "GD"
|
||||||
|
format_description = "GD uncompressed images"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
# Header
|
||||||
|
assert self.fp is not None
|
||||||
|
|
||||||
|
s = self.fp.read(1037)
|
||||||
|
|
||||||
|
if i16(s) not in [65534, 65535]:
|
||||||
|
msg = "Not a valid GD 2.x .gd file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self._mode = "P"
|
||||||
|
self._size = i16(s, 2), i16(s, 4)
|
||||||
|
|
||||||
|
true_color = s[6]
|
||||||
|
true_color_offset = 2 if true_color else 0
|
||||||
|
|
||||||
|
# transparency index
|
||||||
|
tindex = i32(s, 7 + true_color_offset)
|
||||||
|
if tindex < 256:
|
||||||
|
self.info["transparency"] = tindex
|
||||||
|
|
||||||
|
self.palette = ImagePalette.raw(
|
||||||
|
"RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tile = [
|
||||||
|
ImageFile._Tile(
|
||||||
|
"raw",
|
||||||
|
(0, 0) + self.size,
|
||||||
|
7 + true_color_offset + 6 + 256 * 4,
|
||||||
|
"L",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile:
|
||||||
|
"""
|
||||||
|
Load texture from a GD image file.
|
||||||
|
|
||||||
|
:param fp: GD file name, or an opened file handle.
|
||||||
|
:param mode: Optional mode. In this version, if the mode argument
|
||||||
|
is given, it must be "r".
|
||||||
|
:returns: An image instance.
|
||||||
|
:raises OSError: If the image could not be read.
|
||||||
|
"""
|
||||||
|
if mode != "r":
|
||||||
|
msg = "bad mode"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return GdImageFile(fp)
|
||||||
|
except SyntaxError as e:
|
||||||
|
msg = "cannot identify this image file"
|
||||||
|
raise UnidentifiedImageError(msg) from e
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,311 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# standard channel operations
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1996-03-24 fl Created
|
||||||
|
# 1996-08-13 fl Added logical operations (for "1" images)
|
||||||
|
# 2000-10-12 fl Added offset method (from Image.py)
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2000 by Secret Labs AB
|
||||||
|
# Copyright (c) 1996-2000 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
|
def constant(image: Image.Image, value: int) -> Image.Image:
|
||||||
|
"""Fill a channel with a given gray level.
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Image.new("L", image.size, value)
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate(image: Image.Image) -> Image.Image:
|
||||||
|
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return image.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def invert(image: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Invert an image (channel). ::
|
||||||
|
|
||||||
|
out = MAX - image
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image.load()
|
||||||
|
return image._new(image.im.chop_invert())
|
||||||
|
|
||||||
|
|
||||||
|
def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Compares the two images, pixel by pixel, and returns a new image containing
|
||||||
|
the lighter values. ::
|
||||||
|
|
||||||
|
out = max(image1, image2)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_lighter(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Compares the two images, pixel by pixel, and returns a new image containing
|
||||||
|
the darker values. ::
|
||||||
|
|
||||||
|
out = min(image1, image2)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_darker(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Returns the absolute value of the pixel-by-pixel difference between the two
|
||||||
|
images. ::
|
||||||
|
|
||||||
|
out = abs(image1 - image2)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_difference(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Superimposes two images on top of each other.
|
||||||
|
|
||||||
|
If you multiply an image with a solid black image, the result is black. If
|
||||||
|
you multiply with a solid white image, the image is unaffected. ::
|
||||||
|
|
||||||
|
out = image1 * image2 / MAX
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_multiply(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Superimposes two inverted images on top of each other. ::
|
||||||
|
|
||||||
|
out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_screen(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Superimposes two images on top of each other using the Soft Light algorithm
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_soft_light(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Superimposes two images on top of each other using the Hard Light algorithm
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_hard_light(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Superimposes two images on top of each other using the Overlay algorithm
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_overlay(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def add(
|
||||||
|
image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Adds two images, dividing the result by scale and adding the
|
||||||
|
offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
||||||
|
|
||||||
|
out = ((image1 + image2) / scale + offset)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_add(image2.im, scale, offset))
|
||||||
|
|
||||||
|
|
||||||
|
def subtract(
|
||||||
|
image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Subtracts two images, dividing the result by scale and adding the offset.
|
||||||
|
If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
||||||
|
|
||||||
|
out = ((image1 - image2) / scale + offset)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
|
||||||
|
|
||||||
|
|
||||||
|
def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""Add two images, without clipping the result. ::
|
||||||
|
|
||||||
|
out = ((image1 + image2) % MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_add_modulo(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""Subtract two images, without clipping the result. ::
|
||||||
|
|
||||||
|
out = ((image1 - image2) % MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_subtract_modulo(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""Logical AND between two images.
|
||||||
|
|
||||||
|
Both of the images must have mode "1". If you would like to perform a
|
||||||
|
logical AND on an image with a mode other than "1", try
|
||||||
|
:py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask
|
||||||
|
as the second image. ::
|
||||||
|
|
||||||
|
out = ((image1 and image2) % MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_and(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""Logical OR between two images.
|
||||||
|
|
||||||
|
Both of the images must have mode "1". ::
|
||||||
|
|
||||||
|
out = ((image1 or image2) % MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_or(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
"""Logical XOR between two images.
|
||||||
|
|
||||||
|
Both of the images must have mode "1". ::
|
||||||
|
|
||||||
|
out = ((bool(image1) != bool(image2)) % MAX)
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
image1.load()
|
||||||
|
image2.load()
|
||||||
|
return image1._new(image1.im.chop_xor(image2.im))
|
||||||
|
|
||||||
|
|
||||||
|
def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
|
||||||
|
"""Blend images using constant transparency weight. Alias for
|
||||||
|
:py:func:`PIL.Image.blend`.
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Image.blend(image1, image2, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def composite(
|
||||||
|
image1: Image.Image, image2: Image.Image, mask: Image.Image
|
||||||
|
) -> Image.Image:
|
||||||
|
"""Create composite using transparency mask. Alias for
|
||||||
|
:py:func:`PIL.Image.composite`.
|
||||||
|
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Image.composite(image1, image2, mask)
|
||||||
|
|
||||||
|
|
||||||
|
def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
|
||||||
|
"""Returns a copy of the image where data has been offset by the given
|
||||||
|
distances. Data wraps around the edges. If ``yoffset`` is omitted, it
|
||||||
|
is assumed to be equal to ``xoffset``.
|
||||||
|
|
||||||
|
:param image: Input image.
|
||||||
|
:param xoffset: The horizontal distance.
|
||||||
|
:param yoffset: The vertical distance. If omitted, both
|
||||||
|
distances are set to the same value.
|
||||||
|
:rtype: :py:class:`~PIL.Image.Image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
if yoffset is None:
|
||||||
|
yoffset = xoffset
|
||||||
|
image.load()
|
||||||
|
return image._new(image.im.offset(xoffset, yoffset))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,320 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# map CSS3-style colour description strings to RGB
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 2002-10-24 fl Added support for CSS-style color strings
|
||||||
|
# 2002-12-15 fl Added RGBA support
|
||||||
|
# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2
|
||||||
|
# 2004-07-19 fl Fixed gray/grey spelling issues
|
||||||
|
# 2009-03-05 fl Fixed rounding error in grayscale calculation
|
||||||
|
#
|
||||||
|
# Copyright (c) 2002-2004 by Secret Labs AB
|
||||||
|
# Copyright (c) 2002-2004 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
|
||||||
|
"""
|
||||||
|
Convert a color string to an RGB or RGBA tuple. If the string cannot be
|
||||||
|
parsed, this function raises a :py:exc:`ValueError` exception.
|
||||||
|
|
||||||
|
.. versionadded:: 1.1.4
|
||||||
|
|
||||||
|
:param color: A color string
|
||||||
|
:return: ``(red, green, blue[, alpha])``
|
||||||
|
"""
|
||||||
|
if len(color) > 100:
|
||||||
|
msg = "color specifier is too long"
|
||||||
|
raise ValueError(msg)
|
||||||
|
color = color.lower()
|
||||||
|
|
||||||
|
rgb = colormap.get(color, None)
|
||||||
|
if rgb:
|
||||||
|
if isinstance(rgb, tuple):
|
||||||
|
return rgb
|
||||||
|
rgb_tuple = getrgb(rgb)
|
||||||
|
assert len(rgb_tuple) == 3
|
||||||
|
colormap[color] = rgb_tuple
|
||||||
|
return rgb_tuple
|
||||||
|
|
||||||
|
# check for known string formats
|
||||||
|
if re.match("#[a-f0-9]{3}$", color):
|
||||||
|
return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16)
|
||||||
|
|
||||||
|
if re.match("#[a-f0-9]{4}$", color):
|
||||||
|
return (
|
||||||
|
int(color[1] * 2, 16),
|
||||||
|
int(color[2] * 2, 16),
|
||||||
|
int(color[3] * 2, 16),
|
||||||
|
int(color[4] * 2, 16),
|
||||||
|
)
|
||||||
|
|
||||||
|
if re.match("#[a-f0-9]{6}$", color):
|
||||||
|
return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
|
||||||
|
|
||||||
|
if re.match("#[a-f0-9]{8}$", color):
|
||||||
|
return (
|
||||||
|
int(color[1:3], 16),
|
||||||
|
int(color[3:5], 16),
|
||||||
|
int(color[5:7], 16),
|
||||||
|
int(color[7:9], 16),
|
||||||
|
)
|
||||||
|
|
||||||
|
m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||||
|
|
||||||
|
m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||||
|
if m:
|
||||||
|
return (
|
||||||
|
int((int(m.group(1)) * 255) / 100.0 + 0.5),
|
||||||
|
int((int(m.group(2)) * 255) / 100.0 + 0.5),
|
||||||
|
int((int(m.group(3)) * 255) / 100.0 + 0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
m = re.match(
|
||||||
|
r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
from colorsys import hls_to_rgb
|
||||||
|
|
||||||
|
rgb_floats = hls_to_rgb(
|
||||||
|
float(m.group(1)) / 360.0,
|
||||||
|
float(m.group(3)) / 100.0,
|
||||||
|
float(m.group(2)) / 100.0,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
int(rgb_floats[0] * 255 + 0.5),
|
||||||
|
int(rgb_floats[1] * 255 + 0.5),
|
||||||
|
int(rgb_floats[2] * 255 + 0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
m = re.match(
|
||||||
|
r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
from colorsys import hsv_to_rgb
|
||||||
|
|
||||||
|
rgb_floats = hsv_to_rgb(
|
||||||
|
float(m.group(1)) / 360.0,
|
||||||
|
float(m.group(2)) / 100.0,
|
||||||
|
float(m.group(3)) / 100.0,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
int(rgb_floats[0] * 255 + 0.5),
|
||||||
|
int(rgb_floats[1] * 255 + 0.5),
|
||||||
|
int(rgb_floats[2] * 255 + 0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
|
||||||
|
msg = f"unknown color specifier: {repr(color)}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
|
||||||
|
"""
|
||||||
|
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
|
||||||
|
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
|
||||||
|
not color or a palette image, converts the RGB value to a grayscale value.
|
||||||
|
If the string cannot be parsed, this function raises a :py:exc:`ValueError`
|
||||||
|
exception.
|
||||||
|
|
||||||
|
.. versionadded:: 1.1.4
|
||||||
|
|
||||||
|
:param color: A color string
|
||||||
|
:param mode: Convert result to this mode
|
||||||
|
:return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])``
|
||||||
|
"""
|
||||||
|
# same as getrgb, but converts the result to the given mode
|
||||||
|
rgb, alpha = getrgb(color), 255
|
||||||
|
if len(rgb) == 4:
|
||||||
|
alpha = rgb[3]
|
||||||
|
rgb = rgb[:3]
|
||||||
|
|
||||||
|
if mode == "HSV":
|
||||||
|
from colorsys import rgb_to_hsv
|
||||||
|
|
||||||
|
r, g, b = rgb
|
||||||
|
h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
|
||||||
|
return int(h * 255), int(s * 255), int(v * 255)
|
||||||
|
elif Image.getmodebase(mode) == "L":
|
||||||
|
r, g, b = rgb
|
||||||
|
# ITU-R Recommendation 601-2 for nonlinear RGB
|
||||||
|
# scaled to 24 bits to match the convert's implementation.
|
||||||
|
graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
|
||||||
|
if mode[-1] == "A":
|
||||||
|
return graylevel, alpha
|
||||||
|
return graylevel
|
||||||
|
elif mode[-1] == "A":
|
||||||
|
return rgb + (alpha,)
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
|
||||||
|
colormap: dict[str, str | tuple[int, int, int]] = {
|
||||||
|
# X11 colour table from https://drafts.csswg.org/css-color-4/, with
|
||||||
|
# gray/grey spelling issues fixed. This is a superset of HTML 4.0
|
||||||
|
# colour names used in CSS 1.
|
||||||
|
"aliceblue": "#f0f8ff",
|
||||||
|
"antiquewhite": "#faebd7",
|
||||||
|
"aqua": "#00ffff",
|
||||||
|
"aquamarine": "#7fffd4",
|
||||||
|
"azure": "#f0ffff",
|
||||||
|
"beige": "#f5f5dc",
|
||||||
|
"bisque": "#ffe4c4",
|
||||||
|
"black": "#000000",
|
||||||
|
"blanchedalmond": "#ffebcd",
|
||||||
|
"blue": "#0000ff",
|
||||||
|
"blueviolet": "#8a2be2",
|
||||||
|
"brown": "#a52a2a",
|
||||||
|
"burlywood": "#deb887",
|
||||||
|
"cadetblue": "#5f9ea0",
|
||||||
|
"chartreuse": "#7fff00",
|
||||||
|
"chocolate": "#d2691e",
|
||||||
|
"coral": "#ff7f50",
|
||||||
|
"cornflowerblue": "#6495ed",
|
||||||
|
"cornsilk": "#fff8dc",
|
||||||
|
"crimson": "#dc143c",
|
||||||
|
"cyan": "#00ffff",
|
||||||
|
"darkblue": "#00008b",
|
||||||
|
"darkcyan": "#008b8b",
|
||||||
|
"darkgoldenrod": "#b8860b",
|
||||||
|
"darkgray": "#a9a9a9",
|
||||||
|
"darkgrey": "#a9a9a9",
|
||||||
|
"darkgreen": "#006400",
|
||||||
|
"darkkhaki": "#bdb76b",
|
||||||
|
"darkmagenta": "#8b008b",
|
||||||
|
"darkolivegreen": "#556b2f",
|
||||||
|
"darkorange": "#ff8c00",
|
||||||
|
"darkorchid": "#9932cc",
|
||||||
|
"darkred": "#8b0000",
|
||||||
|
"darksalmon": "#e9967a",
|
||||||
|
"darkseagreen": "#8fbc8f",
|
||||||
|
"darkslateblue": "#483d8b",
|
||||||
|
"darkslategray": "#2f4f4f",
|
||||||
|
"darkslategrey": "#2f4f4f",
|
||||||
|
"darkturquoise": "#00ced1",
|
||||||
|
"darkviolet": "#9400d3",
|
||||||
|
"deeppink": "#ff1493",
|
||||||
|
"deepskyblue": "#00bfff",
|
||||||
|
"dimgray": "#696969",
|
||||||
|
"dimgrey": "#696969",
|
||||||
|
"dodgerblue": "#1e90ff",
|
||||||
|
"firebrick": "#b22222",
|
||||||
|
"floralwhite": "#fffaf0",
|
||||||
|
"forestgreen": "#228b22",
|
||||||
|
"fuchsia": "#ff00ff",
|
||||||
|
"gainsboro": "#dcdcdc",
|
||||||
|
"ghostwhite": "#f8f8ff",
|
||||||
|
"gold": "#ffd700",
|
||||||
|
"goldenrod": "#daa520",
|
||||||
|
"gray": "#808080",
|
||||||
|
"grey": "#808080",
|
||||||
|
"green": "#008000",
|
||||||
|
"greenyellow": "#adff2f",
|
||||||
|
"honeydew": "#f0fff0",
|
||||||
|
"hotpink": "#ff69b4",
|
||||||
|
"indianred": "#cd5c5c",
|
||||||
|
"indigo": "#4b0082",
|
||||||
|
"ivory": "#fffff0",
|
||||||
|
"khaki": "#f0e68c",
|
||||||
|
"lavender": "#e6e6fa",
|
||||||
|
"lavenderblush": "#fff0f5",
|
||||||
|
"lawngreen": "#7cfc00",
|
||||||
|
"lemonchiffon": "#fffacd",
|
||||||
|
"lightblue": "#add8e6",
|
||||||
|
"lightcoral": "#f08080",
|
||||||
|
"lightcyan": "#e0ffff",
|
||||||
|
"lightgoldenrodyellow": "#fafad2",
|
||||||
|
"lightgreen": "#90ee90",
|
||||||
|
"lightgray": "#d3d3d3",
|
||||||
|
"lightgrey": "#d3d3d3",
|
||||||
|
"lightpink": "#ffb6c1",
|
||||||
|
"lightsalmon": "#ffa07a",
|
||||||
|
"lightseagreen": "#20b2aa",
|
||||||
|
"lightskyblue": "#87cefa",
|
||||||
|
"lightslategray": "#778899",
|
||||||
|
"lightslategrey": "#778899",
|
||||||
|
"lightsteelblue": "#b0c4de",
|
||||||
|
"lightyellow": "#ffffe0",
|
||||||
|
"lime": "#00ff00",
|
||||||
|
"limegreen": "#32cd32",
|
||||||
|
"linen": "#faf0e6",
|
||||||
|
"magenta": "#ff00ff",
|
||||||
|
"maroon": "#800000",
|
||||||
|
"mediumaquamarine": "#66cdaa",
|
||||||
|
"mediumblue": "#0000cd",
|
||||||
|
"mediumorchid": "#ba55d3",
|
||||||
|
"mediumpurple": "#9370db",
|
||||||
|
"mediumseagreen": "#3cb371",
|
||||||
|
"mediumslateblue": "#7b68ee",
|
||||||
|
"mediumspringgreen": "#00fa9a",
|
||||||
|
"mediumturquoise": "#48d1cc",
|
||||||
|
"mediumvioletred": "#c71585",
|
||||||
|
"midnightblue": "#191970",
|
||||||
|
"mintcream": "#f5fffa",
|
||||||
|
"mistyrose": "#ffe4e1",
|
||||||
|
"moccasin": "#ffe4b5",
|
||||||
|
"navajowhite": "#ffdead",
|
||||||
|
"navy": "#000080",
|
||||||
|
"oldlace": "#fdf5e6",
|
||||||
|
"olive": "#808000",
|
||||||
|
"olivedrab": "#6b8e23",
|
||||||
|
"orange": "#ffa500",
|
||||||
|
"orangered": "#ff4500",
|
||||||
|
"orchid": "#da70d6",
|
||||||
|
"palegoldenrod": "#eee8aa",
|
||||||
|
"palegreen": "#98fb98",
|
||||||
|
"paleturquoise": "#afeeee",
|
||||||
|
"palevioletred": "#db7093",
|
||||||
|
"papayawhip": "#ffefd5",
|
||||||
|
"peachpuff": "#ffdab9",
|
||||||
|
"peru": "#cd853f",
|
||||||
|
"pink": "#ffc0cb",
|
||||||
|
"plum": "#dda0dd",
|
||||||
|
"powderblue": "#b0e0e6",
|
||||||
|
"purple": "#800080",
|
||||||
|
"rebeccapurple": "#663399",
|
||||||
|
"red": "#ff0000",
|
||||||
|
"rosybrown": "#bc8f8f",
|
||||||
|
"royalblue": "#4169e1",
|
||||||
|
"saddlebrown": "#8b4513",
|
||||||
|
"salmon": "#fa8072",
|
||||||
|
"sandybrown": "#f4a460",
|
||||||
|
"seagreen": "#2e8b57",
|
||||||
|
"seashell": "#fff5ee",
|
||||||
|
"sienna": "#a0522d",
|
||||||
|
"silver": "#c0c0c0",
|
||||||
|
"skyblue": "#87ceeb",
|
||||||
|
"slateblue": "#6a5acd",
|
||||||
|
"slategray": "#708090",
|
||||||
|
"slategrey": "#708090",
|
||||||
|
"snow": "#fffafa",
|
||||||
|
"springgreen": "#00ff7f",
|
||||||
|
"steelblue": "#4682b4",
|
||||||
|
"tan": "#d2b48c",
|
||||||
|
"teal": "#008080",
|
||||||
|
"thistle": "#d8bfd8",
|
||||||
|
"tomato": "#ff6347",
|
||||||
|
"turquoise": "#40e0d0",
|
||||||
|
"violet": "#ee82ee",
|
||||||
|
"wheat": "#f5deb3",
|
||||||
|
"white": "#ffffff",
|
||||||
|
"whitesmoke": "#f5f5f5",
|
||||||
|
"yellow": "#ffff00",
|
||||||
|
"yellowgreen": "#9acd32",
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user