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