目录迁移

This commit is contained in:
dongchangxi committed 2025-12-09 16:34:35 +08:00
1 parent 2ea782ed93
commit cc15c76e62
26 files changed
+295 -86

No files matched your search

-27
View File
@@ -1,27 +0,0 @@
from utils.import_all_file import modify_file_dialog_path_and_import_all
from utils.click_soft_button import clickFileIMportShow, clickBegingSlice
import time
from utils.exe_operate import start_exe, click_confirm, close
#"C:/test/10192_small_No4/data"
def BeginSliceing(folderPath):
# 打开3d切片软件
start_exe()
time.sleep(1)
#点击确认按钮
click_confirm()
#先打开导入文件的弹框
clickFileIMportShow()
time.sleep(1)
#在输入路径导入obj文件
isSuccess = modify_file_dialog_path_and_import_all(folderPath)
if not isSuccess:
print("导入文件失败")
exit()
#点击开始切片
time.sleep(5)
clickBegingSlice()
@@ -1,81 +0,0 @@
import uiautomation as auto
# 遍历所有控件 找到
def findAndClickFileImportShow(control, depth=0,index=1):
for child in control.GetChildren():
# print(child)
# print(' ' * depth + f"{child.ControlType}: {child.Name} | {child.AutomationId}")
if child.Name == "切片及打印":
# 点击和 child 同组的第二个元素(即 control 的子控件中的第二个)
siblings = control.GetChildren() # 获取所有同级元素
# print(f"\n找到目标控件: {child.Name}")
# print(f"同级元素数量: {len(siblings)}")
# 打印所有同级元素信息,方便调试
for i, sibling in enumerate(siblings):
marker = " <-- 这是找到的目标" if sibling == child else ""
print(f" 同级元素[{i}]: {sibling.ControlType} | {sibling.Name} | {sibling.AutomationId}{marker}")
# 确保有至少2个同级元素
if len(siblings) >= 2:
target = siblings[index] # 第二个元素(索引从0开始)
print(f"\n准备点击同级{index+1}个元素:")
print(f" 控件类型: {target.ControlType}")
print(f" 控件名称: {target.Name}")
print(f" 自动化ID: {target.AutomationId}")
# 尝试多种点击方式
try:
# 方法1: 直接点击
target.Click()
print("✓ 使用 Click() 方法点击成功")
return True
except Exception as e:
print(f"✗ Click() 失败: {e}")
try:
# 方法2: 使用 Invoke(如果是可调用的控件)
target.Invoke()
print("✓ 使用 Invoke() 方法点击成功")
return True
except Exception as e2:
print(f"✗ Invoke() 失败: {e2}")
try:
# 方法3: 先设置焦点再点击
target.SetFocus()
import time
time.sleep(0.1)
target.Click()
print("✓ 使用 SetFocus() + Click() 方法点击成功")
return True
except Exception as e3:
print(f"✗ SetFocus() + Click() 失败: {e3}")
# 方法4: 使用鼠标点击坐标
try:
rect = target.BoundingRectangle
# BoundingRectangle 是一个矩形对象,计算中心点
x = rect.left + ((rect.right - rect.left) / 2)
y = rect.top + ((rect.bottom - rect.top) / 2)
auto.Click(x, y)
print(f"✓ 使用坐标点击成功: ({x}, {y})")
return True
except Exception as e4:
print(f"✗ 坐标点击失败: {e4}")
else:
print(f"✗ 同级元素数量不足,只有 {len(siblings)} 个")
return False
else:
findAndClickFileImportShow(child, depth + 1)
# 点击导入文件按钮
def clickFileIMportShow():
control = auto.WindowControl(searchDepth=1, Name='赛纳3D打印控制系统 V1.4.3.2')
clickRes = findAndClickFileImportShow(control,index=1)
return clickRes
# 点击开始切片按钮
def clickBegingSlice():
control = auto.WindowControl(searchDepth=1, Name='赛纳3D打印控制系统 V1.4.3.2')
clickRes = findAndClickFileImportShow(control,index=2)
return clickRes
@@ -1,202 +0,0 @@
#启动exe 文件
import os
import subprocess
import time
import uiautomation as auto
exe_path = r"C:\Users\Administrator\Desktop\排版软件\NormalTek.ThreeDPrinter.UI.exe"
def start_exe():
try:
# 检查文件是否存在
if not os.path.exists(exe_path):
print(f"错误:文件不存在 - {exe_path}")
return False
# 检查是否是文件(不是目录)
if not os.path.isfile(exe_path):
print(f"错误:路径不是文件 - {exe_path}")
return False
# 获取 exe 文件所在的目录(工作目录)
exe_dir = os.path.dirname(exe_path)
print(f"工作目录设置为: {exe_dir}")
# 检查工作目录是否存在
if not os.path.exists(exe_dir):
print(f"错误:工作目录不存在 - {exe_dir}")
return False
# 使用 subprocess 启动,并设置工作目录
# 这样可以确保程序能找到同目录下的配置文件
try:
process = subprocess.Popen(
[exe_path],
cwd=exe_dir, # 设置工作目录为 exe 所在目录
creationflags=subprocess.CREATE_NEW_CONSOLE # 在新控制台窗口运行
)
print(f"✓ 成功启动程序: {exe_path}")
print(f" 进程ID: {process.pid}")
print(f" 工作目录: {exe_dir}")
return True
except Exception as e:
print(f"subprocess 启动失败: {e}")
# 备用方案:使用 os.startfile(但无法设置工作目录)
try:
# 先切换到工作目录
original_cwd = os.getcwd()
os.chdir(exe_dir)
os.startfile(exe_path)
os.chdir(original_cwd) # 恢复原目录
print(f"✓ 使用 os.startfile 成功启动程序: {exe_path}")
return True
except Exception as e2:
print(f"os.startfile 启动也失败: {e2}")
return False
except Exception as e:
print(f"启动程序时发生异常: {e}")
import traceback
traceback.print_exc()
return False
def click_confirm():
"""
触发点击 ESC 键,用于关闭弹框
"""
try:
# 发送 ESC 键到当前活动窗口
auto.SendKeys('{ESC}')
print("✓ 已发送 ESC 键")
time.sleep(0.3)
return True
except Exception as e:
print(f"发送 ESC 键失败: {e}")
return False
def close():
"""
关闭打开的应用程序
通过查找窗口名称并关闭窗口,使用多种方法确保成功
"""
try:
import win32gui
import win32con
import win32api
import win32process
# 应用程序的窗口名称
window_name = "赛纳3D打印控制系统 V1.4.3.2"
print(f"正在查找窗口: {window_name}")
# 方法1: 使用 win32gui 查找窗口(更可靠)
hwnd = win32gui.FindWindow(None, window_name)
if hwnd:
print(f"✓ 找到窗口句柄: {hwnd}")
# 激活窗口
win32gui.SetForegroundWindow(hwnd)
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # 确保窗口不是最小化
time.sleep(0.3)
# 方法1: 使用 SendMessage 发送 WM_CLOSE(比 PostMessage 更强制)
try:
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
time.sleep(1)
# 检查窗口是否还存在
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"SendMessage(WM_CLOSE) 失败: {e}")
# 方法2: 使用 win32api 发送 Alt+F4 按键事件(更可靠)
try:
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
# 按下 Alt
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0)
time.sleep(0.05)
# 按下 F4
win32api.keybd_event(win32con.VK_F4, 0, 0, 0)
time.sleep(0.05)
# 释放 F4
win32api.keybd_event(win32con.VK_F4, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.05)
# 释放 Alt
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
print("✓ 使用 win32api 发送 Alt+F4")
time.sleep(1)
# 检查窗口是否还存在
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"win32api Alt+F4 失败: {e}")
# 方法3: 尝试使用 uiautomation 的 Close()
try:
window = auto.WindowControl(searchDepth=1, Name=window_name)
if window.Exists(0, 0):
window.SetFocus()
time.sleep(0.2)
window.Close()
print("✓ 使用 uiautomation Close() 关闭窗口")
time.sleep(1)
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"uiautomation Close() 失败: {e}")
# 方法4: 如果以上都失败,尝试终止进程(最后手段)
print("尝试通过进程ID终止程序...")
try:
import psutil
# 通过窗口句柄获取进程ID
_, pid = win32process.GetWindowThreadProcessId(hwnd)
process = psutil.Process(pid)
process.terminate()
print(f"✓ 已终止进程 (PID: {pid})")
time.sleep(0.5)
return True
except ImportError:
print("psutil 未安装,无法使用进程终止方式")
except Exception as e:
print(f"终止进程失败: {e}")
print("✗ 所有关闭方法都失败了")
return False
else:
# 如果 win32gui 找不到,尝试使用 uiautomation
print("win32gui 未找到窗口,尝试使用 uiautomation...")
window = auto.WindowControl(searchDepth=1, Name=window_name)
if window.Exists(0, 0):
print(f"✓ 使用 uiautomation 找到窗口")
window.SetFocus()
time.sleep(0.2)
hwnd = window.Handle
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
time.sleep(1)
return True
else:
print(f"✗ 未找到窗口: {window_name}")
return False
except Exception as e:
print(f"关闭应用程序时发生异常: {e}")
import traceback
traceback.print_exc()
return False
#if __name__ == "__main__":
# start_exe()
# #启动成功之后,有一个弹框 的窗口,点击确定
# time.sleep(5)
# click_confirm()
# time.sleep(10)
# close()
@@ -1,179 +0,0 @@
import win32gui, win32con, time
import win32api
import ctypes
def find_window(title=None, class_name=None):
# 任一参数为 None 即忽略该条件
hwnd = win32gui.FindWindow(class_name, title)
return hwnd if hwnd else None
def send_text_to_window(hwnd, text):
"""向窗口发送文本"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
for char in text:
# 获取虚拟键码
vk_code = win32api.VkKeyScan(char)
if vk_code != -1:
# 提取低字节(虚拟键码)和高字节(修饰键)
vk = vk_code & 0xFF
shift_state = (vk_code >> 8) & 0xFF
# 如果需要 Shift
if shift_state & 1: # Shift 键
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
time.sleep(0.01)
# 按下键
win32api.keybd_event(vk, 0, 0, 0)
time.sleep(0.01)
# 释放键
win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
# 释放 Shift(如果按下了)
if shift_state & 1:
time.sleep(0.01)
win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.02) # 字符之间的延迟
#发送 Alt+D 快捷键(聚焦地址栏)
def send_alt_d(hwnd):
"""发送 Alt+D 快捷键(聚焦地址栏)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt
time.sleep(0.05)
win32api.keybd_event(ord('D'), 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('D'), 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.3)
#发送 Ctrl+A 快捷键(全选)
def send_ctrl_a(hwnd):
"""发送 Ctrl+A 快捷键(全选)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('A'), 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('A'), 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.3)
#发送 Enter 键
def send_enter(hwnd):
"""发送 Enter 键"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1)
win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.2)
#
def send_tab(hwnd, count=1):
"""发送 Tab 键(用于导航焦点)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1)
for _ in range(count):
win32api.keybd_event(win32con.VK_TAB, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(win32con.VK_TAB, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
time.sleep(0.2)
#聚焦文件列表窗口
def focus_file_list(hwnd):
"""将焦点移到文件列表区域"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
# 使用鼠标点击文件列表区域(最可靠的方法)
try:
# 获取窗口位置和大小
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
width = right - left
height = bottom - top
# 文件列表通常在窗口中间偏下的位置
# 计算文件列表的大概位置(窗口中心偏下,避开地址栏和按钮区域)
click_x = left + width // 2
click_y = top + height // 2 + 30 # 稍微偏下一点,避开地址栏
# 点击文件列表区域
win32api.SetCursorPos((click_x, click_y))
time.sleep(0.1)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.2)
except Exception as e:
print(f"点击文件列表失败,使用Tab键方式: {e}")
# 如果点击失败,使用Tab键导航(备用方案)
send_tab(hwnd, 1)
time.sleep(0.2)
def modify_file_dialog_path_and_import_all(target_path=""):
if not target_path:
return False
"""
修改文件对话框的路径并导入所有文件
参数:
target_path: 目标文件夹路径,默认为 C://work/batchPrint
"""
# 1. 找到"打开"窗口
print("正在查找'打开'窗口...")
hwnd = find_window(title='打开')
if not hwnd:
print("未找到'打开'窗口,请确保文件对话框已打开")
return False
print(f"✓ 找到'打开'窗口,句柄: {hwnd}")
time.sleep(0.5)
# 2. 激活窗口并聚焦地址栏(使用 Alt+D 快捷键)
print("正在聚焦地址栏...")
send_alt_d(hwnd)
time.sleep(0.3)
# 3. 清空地址栏并输入新路径
print(f"正在设置路径为: {target_path}")
send_ctrl_a(hwnd) # 全选现有路径
time.sleep(0.2)
# 输入新路径
send_text_to_window(hwnd, target_path)
time.sleep(0.3)
# 按 Enter 确认路径
send_enter(hwnd)
print("✓ 路径已设置")
time.sleep(1.5) # 等待路径加载完成
# 4. 将焦点移到文件列表
print("正在将焦点移到文件列表...")
focus_file_list(hwnd)
time.sleep(0.3)
# 5. 选择所有文件
print("正在选择所有文件...")
send_ctrl_a(hwnd) # Ctrl+A 选择所有文件
time.sleep(0.5)
print("✓ 已选择所有文件")
# 6. 点击"打开"按钮(使用 Enter 键)
print("正在确认打开...")
send_enter(hwnd)
print("✓ 已确认打开")
return True
@@ -1,4 +0,0 @@
import time
def log(message):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
@@ -1,45 +0,0 @@
import os
from .oss_redis import ossClient
from .logs import log
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
def download_file_with_check(ossFilePath, localFilePath):
try:
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
if not checkFileExists(ossFilePath):
log(f"文件不存在: {ossFilePath}")
return False
# 同步阻塞下载,下载完成后才会返回
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
# 1. 检查 HTTP 状态码
status = getattr(result, "status", None)
if status != 200:
log(f"下载失败,HTTP 状态码异常: status={status}")
return False
# 2. 远端 / 本地文件大小对比,作为二次校验
remote_meta = ossClient().head_object(ossFilePath)
remote_size = getattr(remote_meta, "content_length", None)
if remote_size is None:
log("无法获取远端文件大小,放弃本次下载结果")
return False
if not os.path.exists(localFilePath):
log("本地文件不存在,下载可能失败")
return False
local_size = os.path.getsize(localFilePath)
if remote_size != local_size:
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
return False
log("文件下载成功且完整性校验通过")
return True
except Exception as e:
log(f"下载文件出现异常: {str(e)}")
return False
def checkFileExists(ossFilePath):
return ossClient().object_exists(ossFilePath)
@@ -1,125 +0,0 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': 'mp.api.suwa3d.com',
'password': 'kcV2000',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
def redisClient():
"""获取Redis客户端单例"""
return RedisClientSingleton().get_client()
-59
View File
@@ -1,59 +0,0 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 开始打包 factory_sliceing 为 Windows EXE
echo ========================================
echo.
REM 检查 Python 是否安装
python --version >nul 2>&1
if errorlevel 1 (
echo [错误] 未找到 Python,请先安装 Python
pause
exit /b 1
)
echo [1/4] 检查并安装依赖包...
python -m pip install --upgrade pip
python -m pip install redis>=4.0.0 oss2>=2.17.0 requests>=2.28.0 pyinstaller>=5.0.0
if errorlevel 1 (
echo [错误] 依赖包安装失败
pause
exit /b 1
)
echo.
echo [2/4] 清理之前的构建文件...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist __pycache__ rmdir /s /q __pycache__
for /d /r . %%d in (__pycache__) do @if exist "%%d" rmdir /s /q "%%d"
echo.
echo [3/4] 使用 PyInstaller 打包...
pyinstaller build_exe.spec --clean --noconfirm
if errorlevel 1 (
echo [错误] 打包失败
pause
exit /b 1
)
echo.
echo [4/4] 打包完成!
echo.
echo ========================================
echo 打包结果:
echo ========================================
echo EXE 文件位置: dist\factory_sliceing.exe
echo.
echo 使用说明:
echo 1. 将 dist\factory_sliceing.exe 复制到目标 Windows 机器
echo 2. 在命令行中运行: factory_sliceing.exe --work-dir D:\work
echo.
echo ========================================
pause
-152
View File
@@ -1,152 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
打包脚本 - 将 main.py 打包成 Windows EXE 文件
使用方法: python build_exe.py
"""
import os
import sys
import subprocess
import shutil
def check_python_version():
"""检查 Python 版本"""
if sys.version_info < (3, 7):
print("[错误] 需要 Python 3.7 或更高版本")
return False
print(f"[信息] Python 版本: {sys.version}")
return True
def install_dependencies():
"""安装依赖包"""
print("\n[1/4] 检查并安装依赖包...")
dependencies = [
'redis>=4.0.0',
'oss2>=2.17.0',
'requests>=2.28.0',
'pyinstaller>=5.0.0',
]
for dep in dependencies:
print(f" 安装 {dep}...")
result = subprocess.run(
[sys.executable, '-m', 'pip', 'install', '--upgrade', dep],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"[错误] 安装 {dep} 失败: {result.stderr}")
return False
print("[成功] 所有依赖包已安装")
return True
def clean_build_files():
"""清理之前的构建文件"""
print("\n[2/4] 清理之前的构建文件...")
dirs_to_remove = ['build', 'dist']
for dir_name in dirs_to_remove:
if os.path.exists(dir_name):
shutil.rmtree(dir_name)
print(f" 已删除: {dir_name}")
# 清理 __pycache__ 目录
for root, dirs, files in os.walk('.'):
if '__pycache__' in dirs:
pycache_path = os.path.join(root, '__pycache__')
shutil.rmtree(pycache_path)
print(f" 已删除: {pycache_path}")
print("[成功] 清理完成")
return True
def build_exe():
"""使用 PyInstaller 打包"""
print("\n[3/4] 使用 PyInstaller 打包...")
spec_file = 'build_exe.spec'
if not os.path.exists(spec_file):
print(f"[错误] 未找到配置文件: {spec_file}")
return False
result = subprocess.run(
[sys.executable, '-m', 'PyInstaller', spec_file, '--clean', '--noconfirm'],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"[错误] 打包失败:")
print(result.stderr)
return False
print("[成功] 打包完成")
return True
def show_result():
"""显示打包结果"""
print("\n" + "="*50)
print("打包结果:")
print("="*50)
exe_path = os.path.join('dist', 'factory_sliceing.exe')
if os.path.exists(exe_path):
file_size = os.path.getsize(exe_path) / (1024 * 1024) # MB
print(f"✓ EXE 文件位置: {exe_path}")
print(f"✓ 文件大小: {file_size:.2f} MB")
else:
print(f"✗ 未找到 EXE 文件: {exe_path}")
print("\n使用说明:")
print(" 1. 将 dist/factory_sliceing.exe 复制到目标 Windows 机器")
print(" 2. 在命令行中运行: factory_sliceing.exe --work-dir D:\\work")
print("="*50)
def main():
"""主函数"""
print("="*50)
print("开始打包 factory_sliceing 为 Windows EXE")
print("="*50)
# 切换到脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print(f"[信息] 工作目录: {script_dir}")
# 检查 Python 版本
if not check_python_version():
return 1
# 安装依赖
if not install_dependencies():
return 1
# 清理构建文件
if not clean_build_files():
return 1
# 打包
if not build_exe():
return 1
# 显示结果
show_result()
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except KeyboardInterrupt:
print("\n\n[中断] 用户取消操作")
sys.exit(1)
except Exception as e:
print(f"\n[错误] 发生异常: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
-58
View File
@@ -1,58 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
# 将 small_machine_transform.py 作为数据文件包含,以便 Blender 可以调用
('utils/small_machine_transform.py', 'utils'),
],
hiddenimports=[
'redis',
'oss2',
'requests',
'utils.oss_redis',
'utils.funcs',
'utils.logs',
'utils.oss_func',
'utils.changeFiles',
'utils.small_machine_transform',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='factory_sliceing',
debug=False,
bootloader_ignore_signals=False, # 允许信号传播,使 Ctrl+C 可以中断
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # 显示控制台窗口
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None, # 可以指定图标文件路径,例如: 'icon.ico'
)
Binary file not shown.
-158
View File
@@ -1,158 +0,0 @@
import os,shutil
import redis
import oss2,time,sys
import requests
import argparse,json
from utils.funcs import requestApiToUpdateSliceStatus
# 将当前脚本所在目录添加到 Python 路径,以便导入 utils 模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# from download_print_out import download_transform_save_by_json
from utils.oss_redis import redisClient
from utils.funcs import downloadJsonAndJpgFileAndMoveToCorrectDir, downloadDataByOssAndTransformSave
# 默认使用脚本所在目录
currentDir = os.path.dirname(os.path.abspath(__file__))
ENV = 'prod'
url = 'https://mp.api.suwa3d.com'
if ENV == 'dev':
url = 'http://mp.api.dev.com'
elif ENV == 'prod':
url = 'https://mp.api.suwa3d.com'
#判断是否上传了 JSON 文件
def step1(versionId):
# 下载json 文件 和 图片
dirName,machineInfo = downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir)
if not dirName:
return False
#判断是否是小机台
isSmallMachine = False
if str(machineInfo["machine_type"]) == '1':
isSmallMachine = True
#下载数据,转换数据
res = downloadDataByOssAndTransformSave(dirName,isSmallMachine)
if not res:
return False
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
objFilePath = os.path.join(dirName, 'data')
objCounts = 0
for file in os.listdir(objFilePath):
if file == 'arrange':
continue
if file.endswith('.obj'):
objCounts += 1
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}')
# requestApiToUpdateSliceStatus(versionId,objCounts)
# 读取 队列中一个数据出来
def main(work_dir=None, batch_id=None):
global currentDir
# 如果指定了工作目录,使用指定的目录
if work_dir:
work_dir = os.path.abspath(work_dir)
if not os.path.exists(work_dir):
print(f'指定的工作目录不存在: {work_dir},将创建该目录')
os.makedirs(work_dir, exist_ok=True)
currentDir = work_dir
print(f'使用指定的工作目录: {currentDir}')
else:
print(f'没有指定工作目录,退出')
exit(0)
versionId = str(batch_id)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
res = step1(versionId)
if res == False:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
# time.sleep(10)
# continue
# 循环处理,直到队列为空
# try:
# while True:
# try:
# r = redisClient()
# #检测队列是否有值
# if r.scard('pb:sliceing') == 0:
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 队列为空,等待10秒')
# time.sleep(10)
# continue
# #获取队列中的值
# data = r.spop('pb:sliceing')
# if data is None:
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据为空,等待10秒')
# time.sleep(10)
# continue
# data = data.decode('utf-8')
# #判断是否是数字
# if not data.isdigit():
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据不是数字,等待10秒')
# time.sleep(10)
# continue
# versionId = str(data)
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
# res = step1(versionId)
# if res == False:
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
# time.sleep(10)
# continue
# # 在长时间操作后,确保 Redis 连接仍然有效
# # 通过重新获取客户端来触发连接检查
# try:
# r = redisClient()
# r.ping() # 测试连接
# except Exception as e:
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
# #time.sleep(10)
# except KeyboardInterrupt:
# # 在循环内部捕获 KeyboardInterrupt,允许在 sleep 或操作中被中断
# raise # 重新抛出,让外层捕获
# except KeyboardInterrupt:
# print(f'\n{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 收到中断信号,正在优雅退出...')
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 程序已停止')
# sys.exit(0)
def testMain():
global currentDir
currentDir = "/Users/dcx/code/make2/script/factory_sliceing_v2/tempData"
versionId = '10153' #'10153 10158'
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
res = step1(versionId)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 处理完成,res={res}')
if __name__ == '__main__':
#testMain() 新增参数 batchId
parser = argparse.ArgumentParser(description='排版打印订单处理程序')
parser.add_argument(
'--work-dir',
type=str,
default=None,
help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录'
)
parser.add_argument(
'--batch—id',
type=str,
default=None,
help='指定批次ID'
)
args = parser.parse_args()
main(work_dir=args.work_dir, batch_id=args.batch_id)
-144
View File
@@ -1,144 +0,0 @@
import os,shutil
import redis
import oss2,time,sys
import requests
import argparse,json
from utils.funcs import requestApiToUpdateSliceStatus
# 将当前脚本所在目录添加到 Python 路径,以便导入 utils 模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# from download_print_out import download_transform_save_by_json
from utils.oss_redis import redisClient
from utils.funcs import downloadJsonAndJpgFileAndMoveToCorrectDir, downloadDataByOssAndTransformSave
# 默认使用脚本所在目录
currentDir = os.path.dirname(os.path.abspath(__file__))
ENV = 'prod'
url = 'https://mp.api.suwa3d.com'
if ENV == 'dev':
url = 'http://mp.api.dev.com'
elif ENV == 'prod':
url = 'https://mp.api.suwa3d.com'
#判断是否上传了 JSON 文件
def step1(versionId):
# 下载json 文件 和 图片
dirName,machineInfo = downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir)
if not dirName:
return False
#判断是否是小机台
isSmallMachine = False
if str(machineInfo["machine_type"]) == '1':
isSmallMachine = True
#下载数据,转换数据
res = downloadDataByOssAndTransformSave(dirName,isSmallMachine)
if not res:
return False
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
objFilePath = os.path.join(dirName, 'data')
objCounts = 0
for file in os.listdir(objFilePath):
if file == 'arrange':
continue
if file.endswith('.obj'):
objCounts += 1
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}')
requestApiToUpdateSliceStatus(versionId,objCounts)
# 读取 队列中一个数据出来
def main(work_dir=None):
global currentDir
# 如果指定了工作目录,使用指定的目录
if work_dir:
work_dir = os.path.abspath(work_dir)
if not os.path.exists(work_dir):
print(f'指定的工作目录不存在: {work_dir},将创建该目录')
os.makedirs(work_dir, exist_ok=True)
currentDir = work_dir
print(f'使用指定的工作目录: {currentDir}')
else:
print(f'没有指定工作目录,退出')
exit(0)
# 循环处理,直到队列为空
try:
while True:
try:
r = redisClient()
#检测队列是否有值
if r.scard('pb:sliceing') == 0:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 队列为空,等待10秒')
time.sleep(10)
continue
#获取队列中的值
data = r.spop('pb:sliceing')
if data is None:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据为空,等待10秒')
time.sleep(10)
continue
data = data.decode('utf-8')
#判断是否是数字
if not data.isdigit():
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据不是数字,等待10秒')
time.sleep(10)
continue
versionId = str(data)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
res = step1(versionId)
if res == False:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
time.sleep(10)
continue
# 在长时间操作后,确保 Redis 连接仍然有效
# 通过重新获取客户端来触发连接检查
try:
r = redisClient()
r.ping() # 测试连接
except Exception as e:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
#time.sleep(10)
except KeyboardInterrupt:
# 在循环内部捕获 KeyboardInterrupt,允许在 sleep 或操作中被中断
raise # 重新抛出,让外层捕获
except KeyboardInterrupt:
print(f'\n{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 收到中断信号,正在优雅退出...')
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 程序已停止')
sys.exit(0)
def testMain():
global currentDir
currentDir = "/Users/dcx/code/make2/script/factory_sliceing_v2/tempData"
versionId = '10153' #'10153 10158'
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
res = step1(versionId)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 处理完成,res={res}')
if __name__ == '__main__':
#testMain()
parser = argparse.ArgumentParser(description='排版打印订单处理程序')
parser.add_argument(
'--work-dir',
type=str,
default=None,
help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录'
)
args = parser.parse_args()
main(work_dir=args.work_dir)
@@ -1,7 +0,0 @@
redis>=4.0.0
oss2>=2.17.0
requests>=2.28.0
pyinstaller>=5.0.0
@@ -1,100 +0,0 @@
import os,time
import re
from .logs import log
def changeMtlFile(localMtlPath,newName):
beginTime = time.time()
try:
# 使用更高效的文件读取方式 [6,8](@ref)
with open(localMtlPath, 'r', encoding='utf-8') as f:
content = f.read()
# 使用字符串方法直接查找和替换,避免不必要的循环 [9](@ref)
lines = content.split('\n')
for i, line in enumerate(lines):
stripped_line = line.strip()
if stripped_line.startswith('map_Kd '):
lines[i] = f"map_Kd {newName}"
break
# 批量写入,减少I/O操作 [6](@ref)
with open(localMtlPath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
except IOError as e:
log(f"处理文件 {localMtlPath} 时出错: {e}")
return False
except UnicodeDecodeError as e:
log(f"文件编码错误 {localMtlPath}: {e}")
return False
endTime = time.time()
log(f"修改文件文件内容 {localMtlPath} : 耗时{endTime - beginTime}秒")
return True
def changeObjFile(localObjPath,newName):
beginTime = time.time()
try:
# 优化策略:mtllib通常在文件前几行,先只读取前100行
# 对于大文件(如155万行),这样可以大幅减少内存使用和处理时间
with open(localObjPath, 'r', encoding='utf-8') as f:
first_100_lines = []
mtllib_line_idx = -1
# 读取前100行查找mtllib
for i in range(100):
line = f.readline()
if not line:
break
first_100_lines.append(line)
if mtllib_line_idx == -1 and line.strip().startswith('mtllib '):
mtllib_line_idx = i
# 如果在前100行找到了mtllib,直接修改
if mtllib_line_idx != -1:
parts = first_100_lines[mtllib_line_idx].split(' ', 1)
if len(parts) > 1:
#old_mtl_name = parts[1].strip()
new_mtl_name = f"{newName}"
first_100_lines[mtllib_line_idx] = f"mtllib {new_mtl_name}\n"
# 读取剩余内容并合并
remaining_content = f.read()
# 写回文件
with open(localObjPath, 'w', encoding='utf-8') as f_out:
f_out.writelines(first_100_lines)
if remaining_content:
f_out.write(remaining_content)
endTime = time.time()
log(f"修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
return True
log(f"浪费时间读取完整的文件,进行替换mtllib路径, localObjPath={localObjPath}, newName={newName}")
# 如果前100行没找到,读取整个文件(很少见的情况)
f.seek(0) # 重置文件指针
content = f.read()
# 使用正则表达式一次性替换(更高效)
pattern = r'^mtllib\s+(.+)$'
def replace_mtllib(match):
old_mtl_name = match.group(1).strip()
new_mtl_name = f"{newName}_{old_mtl_name}"
return f"mtllib {new_mtl_name}"
new_content = re.sub(pattern, replace_mtllib, content, flags=re.MULTILINE)
if new_content != content:
with open(localObjPath, 'w', encoding='utf-8') as f_out:
f_out.write(new_content)
endTime = time.time()
log(f"完整读取修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
return True
except IOError as e:
print(f"处理文件 {localObjPath} 时出错: {e}")
return False
except UnicodeDecodeError as e:
print(f"文件编码错误 {localObjPath}: {e}")
return False
return False
-688
View File
@@ -1,688 +0,0 @@
import requests
import time
import os
import platform
import shutil
import json
import shlex
import subprocess
import sys
import signal
from concurrent.futures import ThreadPoolExecutor, as_completed
from .oss_redis import ossClient
from .logs import log
from .oss_func import download_file_with_check, checkFileExists
from .changeFiles import changeObjFile, changeMtlFile
from .small_machine_transform import transform_save
ENV = 'prod'
url = 'https://mp.api.suwa3d.com'
if ENV == 'dev':
url = 'http://mp.api.dev.com'
elif ENV == 'prod':
url = 'https://mp.api.suwa3d.com'
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径,兼容 PyInstaller 打包后的环境
参数:
relative_path: 相对于脚本目录的相对路径
返回:
资源文件的绝对路径
"""
try:
# PyInstaller 创建临时文件夹,并将路径存储在 _MEIPASS 中
base_path = sys._MEIPASS
except AttributeError:
# 如果不是打包环境,使用脚本所在目录
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
def get_transform_script_path():
"""
获取 small_machine_transform.py 脚本的路径
在打包成exe后,如果脚本不在可访问位置,则将其提取到临时目录
返回:
transform_script_path: 脚本的绝对路径
"""
# 首先尝试从资源路径获取
if getattr(sys, 'frozen', False):
# 打包后的环境
# 尝试从资源路径获取
resource_path = get_resource_path('utils/small_machine_transform.py')
# 如果资源路径存在,直接使用
if os.path.exists(resource_path):
return resource_path
# 如果不存在,尝试提取到临时目录
import tempfile
temp_dir = tempfile.gettempdir()
extract_path = os.path.join(temp_dir, 'factory_sliceing_utils', 'small_machine_transform.py')
# 确保目录存在
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
# 如果提取路径不存在,尝试从资源中复制
if not os.path.exists(extract_path):
# 尝试从多个可能的资源路径查找
possible_paths = [
get_resource_path('small_machine_transform.py'),
get_resource_path('utils/small_machine_transform.py'),
]
for src_path in possible_paths:
if os.path.exists(src_path):
shutil.copy2(src_path, extract_path)
log(f"已将 small_machine_transform.py 提取到: {extract_path}")
return extract_path
# 如果提取路径存在,直接使用
if os.path.exists(extract_path):
return extract_path
# 如果都找不到,返回资源路径(即使不存在,让调用者处理错误)
return resource_path
else:
# 开发环境,直接使用相对路径
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, 'small_machine_transform.py')
# 根据打印ID 获取下载目录
def getDownloadDirByPrintId(printIds):
# 调用接口获取下载的路径
api_url = f"{url}/api/printOrder/getInfoByPrintIds"
res = requests.post(api_url, json={"print_ids": printIds})
res = res.json()
print(f"根据打印ID 获取下载目录, res={res}")
if res["code"] == 1000:
return res["data"]
else:
return False
# 根据批次id,进行切片中的状态变更
def requestApiToUpdateSliceStatus(versionId, downloadCounts):
api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&download_counts=" + str(downloadCounts)
log(f'发起状态变更请求url={api_url}, versionId={versionId}')
try:
# 添加超时参数,防止请求时间过长
res = requests.post(api_url, timeout=60) # 60秒超时
if res.status_code != 200:
log(f'状态变更请求失败, res={res.text}')
return False
log(f'状态变更请求成功, res={res.text}')
return True
except requests.exceptions.Timeout:
log(f'状态变更请求超时, url={api_url}')
return False
except requests.exceptions.RequestException as e:
log(f'状态变更请求异常, error={str(e)}')
return False
# 判断是否上传了 JSON 文件
def checkJsonFileExists(versionId):
log(f"检测文件和图片是否存在, versionId={versionId}")
jsonFilePath = f'batchPrint/{versionId}/{versionId}.json'
# 判断oss 上是否存在
jpgFilePath = f'batchPrint/{versionId}/{versionId}.jpg'
if not ossClient().object_exists(jsonFilePath):
log(f'JSON文件不存在: {jsonFilePath}')
return False, False
if not ossClient().object_exists(jpgFilePath):
log(f'JPG文件不存在: {jpgFilePath}')
return False, False
log(f"文件和图片检测成功,存在, versionId={versionId}")
return jsonFilePath, jpgFilePath
# 检测本地文件是否存在
def checkLocalFileExists(localFilePath):
if not os.path.exists(localFilePath):
return False
return True
# 读取JSON文件内容
def readJsonFile(localFilePath):
with open(localFilePath, 'r', encoding='utf-8') as f:
jsonData = json.load(f)
return jsonData
# 根据批次ID,获取批次信息
def getBatchInfo(versionId):
url1 = f"{url}/api/printTypeSettingOrder/getBatchInfoAndPrintMachineInfoByBatchId?batch_id={versionId}"
res = requests.get(url1)
res = res.json()
log(f"获取批次信息和打印机信息, url={url1}, res={res}")
if res["code"] == 1000:
return res["data"]
else:
return False
# 下载文件,读取文件内容,并且文件迁移至正确的目录里,不再放在临时目录里
def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId, currentDir):
jsonFilePath, jpgFilePath = checkJsonFileExists(versionId)
if jsonFilePath == False or jpgFilePath == False:
return False, False
# 将文件下载到临时目录,待会要判断是否什么类型机型
tempDir = os.path.join(currentDir, 'batchPrint', 'temp', versionId)
if not os.path.exists(tempDir):
os.makedirs(tempDir)
localFilePath = os.path.join(tempDir, f'{versionId}.json')
# 使用带完整性校验的下载方法下载JSON文件
ok = download_file_with_check(jsonFilePath, localFilePath)
if not ok:
log(f"JSON 文件下载失败或不完整, versionId={versionId}")
return False, False
# 下载JPG文件
localJpgFilePath = os.path.join(tempDir, f'{versionId}.jpg')
ok = download_file_with_check(jpgFilePath, localJpgFilePath)
if not ok:
log(f"JPG 文件下载失败或不完整, versionId={versionId}")
return False, False
# 根据批次ID,获取批次信息和打印机信息
batchMachineInfo = getBatchInfo(versionId)
if not batchMachineInfo:
log(f"获取批次信息和打印机信息失败, versionId={versionId}")
return False, False
# "batch_info": batchInfo,
# "print_machine_info": printMachineInfo,
machineInfo = batchMachineInfo["print_machine_info"]
print(f"machineInfo={machineInfo['id']}")
dirNewName = ""
if str(machineInfo["machine_type"]) == '1':
dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_small_No' + str(machineInfo['id']))
else:
dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_big_No' + str(machineInfo['id']))
# 判断目录是否存在,存在就删除
if os.path.exists(dirNewName):
shutil.rmtree(dirNewName)
# 创建目录
os.makedirs(dirNewName)
# 创建json子目录
jsonSubDir = os.path.join(dirNewName, 'json')
if not os.path.exists(jsonSubDir):
os.makedirs(jsonSubDir)
dataSubDir = os.path.join(dirNewName, 'data')
if not os.path.exists(dataSubDir):
os.makedirs(dataSubDir)
# 将数据移动过来
shutil.move(localFilePath, os.path.join(jsonSubDir, '3DPrintLayout.json'))
shutil.move(localJpgFilePath, os.path.join(jsonSubDir, f'{versionId}.jpg'))
#如果是大机台,将 json 文件复制到 data 目录下
if str(machineInfo["machine_type"]) != '1':
shutil.copy(os.path.join(jsonSubDir, '3DPrintLayout.json'), os.path.join(dataSubDir, '3DPrintLayout.json'))
if not os.path.exists(os.path.join(dataSubDir, '3DPrintLayout.json')):
log(f"JSON文件不存在, dataSubDir={dataSubDir}")
return False, False
# 检测文件是否移动成功
if not os.path.exists(os.path.join(jsonSubDir, '3DPrintLayout.json')):
log(f"JSON文件不存在, versionId={versionId}")
return False, False
if not os.path.exists(os.path.join(jsonSubDir, f'{versionId}.jpg')):
log(f"JPG文件不存在, versionId={versionId}")
return False, False
log(f"文件移动成功, versionId={versionId}")
# 返回目录路径
return dirNewName, machineInfo
# 整合json文件,读取对应的数据,返回数据结构
def getJsonData(dirNewName):
jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json')
if not os.path.exists(jsonFilePath):
log(f"JSON文件不存在, dirNewName={dirNewName}")
return False
# 读取JSON文件内容
jsonData = readJsonFile(jsonFilePath)
# 读取models
models = jsonData.get('models', [])
if not models:
log(f"models不存在, dirNewName={dirNewName}")
return False
listData = []
# 遍历models
for model in models:
file_name = model.get('file_name', '')
# 分割数据
arrFileName = file_name.split('_')
orderId = arrFileName[0]
pid = arrFileName[1]
printId = arrFileName[2].replace("P", "")
size = arrFileName[3]
counts = arrFileName[4].replace("x", "").replace(".obj", "")
# 检测这些数据
if not orderId or not pid or not printId or not size or not counts:
log(f"数据不完整, orderId={orderId}, pid={pid}, printId={printId}, size={size}, counts={counts}")
return False
# 创建数据结构
modelInfo = {
"orderId": orderId,
"printId": printId,
"pid": pid,
"size": size,
"counts": counts,
"file_name": file_name,
}
listData.append(modelInfo)
# 检测数据长度
if len(listData) == 0:
log(f"数据长度为0, dirNewName={dirNewName}")
return False
# 返回数据结构
return listData
# 读取 json 文件,获取 homo_matrix 数据,根据 file_name 获取 homo_matrix 数据
def getHomoMatrixByFileName(dirNewName, fileName):
jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json')
if not os.path.exists(jsonFilePath):
log(f"JSON文件不存在, dirNewName={dirNewName}")
return False
# 读取JSON文件内容
jsonData = readJsonFile(jsonFilePath)
if not jsonData:
log(f"读取JSON文件内容失败, dirNewName={dirNewName}")
return False
for model in jsonData["models"]:
#log(f"jsonData={model['file_name']} == {fileName}")
if model["file_name"] == fileName:
log(f"model={model['transform']}")
return model["transform"]["homo_matrix"]
return False
# 处理单个数据项:下载文件、修改关联路径、转换数据(如果是小机台)
def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine):
"""
处理单个数据项的函数,用于多线程处理
参数:
v: 单个数据项
arrDownloadPath: 下载路径数组
dirNewName: 目录名称
dirPath: 数据目录路径
isSmallMachine: 是否是小机台
返回:
(success: bool, error_msg: str)
"""
try:
# 查找匹配的下载路径信息
info = {}
for tempv in arrDownloadPath:
if str(tempv["print_order_id"]) == str(v["printId"]):
info = tempv
break
if not info:
return False, f"未找到匹配的下载路径信息, printId={v['printId']}"
filePath = info["path"]
pid = info["pid"]
orderId = info["order_id"]
printId = info["print_order_id"]
size = info["real_size"]
counts = info["quantity"]
fileName = v["file_name"]
# 判断文件是否存在
ossJpgFilePath = f"{filePath}/printId_{printId}Tex1.jpg"
if not checkFileExists(ossJpgFilePath):
ossJpgFilePath = f"{filePath}/{pid}Tex1.jpg"
localJpgName = os.path.join(f"{orderId}_{pid}Tex1.jpg")
loaclMtlName = os.path.join(f"{orderId}_{pid}.mtl")
localObjName = fileName
arrDownloadFiles = [
{"ossPath": ossJpgFilePath, "localPath": os.path.join(dirPath, localJpgName)},
{"ossPath": f"{filePath}/{pid}.obj", "localPath": os.path.join(dirPath, localObjName)},
{"ossPath": f"{filePath}/{pid}.mtl", "localPath": os.path.join(dirPath, loaclMtlName)},
]
# 遍历下载文件
beginTime = time.time()
objsLocal = ""
for objFiles in arrDownloadFiles:
downloadBeginTime = time.time()
# 判断 mtl 和 jpg 文件是否存在,存在就不在下载了
if "mtl" in objFiles["localPath"] or "jpg" in objFiles["localPath"]:
if os.path.exists(objFiles["localPath"]):
continue
downloadOk = download_file_with_check(objFiles["ossPath"], objFiles["localPath"])
if not downloadOk:
error_msg = f"下载文件失败, ossPath={objFiles['ossPath']}, localPath={objFiles['localPath']}"
log(error_msg)
return False, error_msg
log(f"下载文件耗时: {time.time() - downloadBeginTime}秒 - {objFiles['localPath']}")
# 下载成功之后要修改文件之间的关联路径
if objFiles["localPath"].endswith(".obj"):
beginChangeTime = time.time()
objsLocal = objFiles["localPath"]
changeObjFile(objFiles["localPath"], f"{orderId}_{pid}.mtl")
log(f"修改obj关联耗时: {time.time() - beginChangeTime}秒 - {objFiles['localPath']}")
if objFiles["localPath"].endswith(".mtl"):
changeMtlFile(objFiles["localPath"], f"{orderId}_{pid}Tex1.jpg")
endTime = time.time()
log(f"下载文件和修改文件之间的关联路径 : 耗时{endTime - beginTime}秒 - {fileName}")
# 如果是小机台,则要转换数据
if isSmallMachine:
timeBegin = time.time()
homo_matrix = getHomoMatrixByFileName(dirNewName, localObjName)
if not homo_matrix:
error_msg = f"获取homo_matrix失败, dirNewName={dirNewName}, objsLocal={objsLocal}"
log(error_msg)
return False, error_msg
# 通过blender 调用执行 python 文件
blender_bin_path = findBpyModule()
# 获取 small_machine_transform.py 的绝对路径(兼容打包环境)
transform_script_path = get_transform_script_path()
# 将 homo_matrix 转换为 JSON 字符串
homo_matrix_json = json.dumps(homo_matrix)
# 检查必要文件是否存在
if not blender_bin_path or not os.path.exists(blender_bin_path):
error_msg = f"Blender 可执行文件不存在: {blender_bin_path}"
log(error_msg)
return False, error_msg
if not os.path.exists(transform_script_path):
error_msg = f"转换脚本文件不存在: {transform_script_path}"
log(error_msg)
return False, error_msg
if not os.path.exists(objsLocal):
error_msg = f"OBJ 文件不存在: {objsLocal}"
log(error_msg)
return False, error_msg
# 构建命令参数列表(使用列表形式,避免 shell 转义问题)
cmd = [
blender_bin_path,
'--background',
'--python',
transform_script_path,
'--',
f'--objPathName={objsLocal}',
f'--trans={homo_matrix_json}'
]
log(f"准备执行 Blender 命令")
log(f" Blender路径: {blender_bin_path}")
log(f" 脚本路径: {transform_script_path}")
log(f" OBJ文件: {objsLocal}")
log(f" 完整命令: {' '.join(cmd[:4])} ... (参数已隐藏)")
try:
log(f"开始执行 Blender 转换命令...")
# 使用 subprocess.Popen 以便更好地控制进程和信号处理
# 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误
# 准备进程启动参数
startupinfo = None
preexec_fn = None
if platform.system() == 'Windows':
# Windows 上,不设置 CREATE_NEW_PROCESS_GROUP,以便 Ctrl+C 可以传播
import subprocess as sp
startupinfo = sp.STARTUPINFO()
startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = sp.SW_HIDE
else:
# Unix 系统,设置 preexec_fn 以确保信号处理正确
preexec_fn = os.setsid
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定 UTF-8 编码
errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常
startupinfo=startupinfo,
preexec_fn=preexec_fn
)
try:
# 等待进程完成,捕获输出
stdout, stderr = process.communicate(timeout=300) # 5分钟超时
returncode = process.returncode
except subprocess.TimeoutExpired:
# 超时,终止进程
log(f"Blender 命令执行超时,正在终止进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise
except KeyboardInterrupt:
# 收到中断信号,终止进程
log(f"收到中断信号,正在终止 Blender 进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise # 重新抛出,让调用者处理
# 创建结果对象(模拟 subprocess.run 的返回值)
class Result:
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
result = Result(returncode, stdout, stderr)
log(f"Blender 命令执行完成, 返回码: {result.returncode}")
# 无论成功失败都输出详细信息
if result.stdout:
log(f"Blender stdout 输出: {result.stdout}")
if result.stderr:
log(f"Blender stderr 输出: {result.stderr}")
if result.returncode != 0:
error_output = result.stderr if result.stderr else result.stdout
error_msg = f"调用blender 执行 python 文件失败, 返回码={result.returncode}, fileName={fileName}"
if error_output:
error_msg += f", 错误信息: {error_output[:10000000]}" # 增加错误信息长度限制
log(error_msg)
return False, error_msg
log(f"调用blender 执行 python 文件成功, fileName={fileName}")
except subprocess.TimeoutExpired:
error_msg = f"调用blender 执行超时(超过5分钟), fileName={fileName}"
log(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"调用blender 执行时发生异常: {str(e)}, fileName={fileName}"
log(error_msg)
return False, error_msg
timeEnd = time.time()
log(f"转换数据时间: 耗时{timeEnd - timeBegin}秒 - {objsLocal}")
return True, ""
except Exception as e:
error_msg = f"处理数据项时发生异常, fileName={v.get('file_name', 'unknown')}, error={str(e)}"
log(error_msg)
return False, error_msg
# json文件进行下载对应的数据 和转换数据,传递目录路径
def downloadDataByOssAndTransformSave(dirNewName, isSmallMachine=False, max_workers=1):
listData = getJsonData(dirNewName)
if not listData:
log(f"获取数据失败, dirNewName={dirNewName}")
return False
# 遍历数据
arrPrintId = []
arrPrintDataInfo = []
for modelInfo in listData:
arrPrintId.append(modelInfo.get('printId'))
arrPrintDataInfo.append({
"printId": modelInfo.get('printId'),
"file_name": modelInfo.get('file_name'),
})
# 调用接口获取下载的路径
arrDownloadPath = getDownloadDirByPrintId(arrPrintId)
if not arrDownloadPath:
log(f"获取下载路径失败, arrPrintId={arrPrintId}")
return False
dirPath = os.path.join(dirNewName, 'data')
if not os.path.exists(dirPath):
os.makedirs(dirPath)
# 使用多线程并发处理数据
log(f"开始多线程处理数据, 共{len(listData)}个项目, 线程数={max_workers}")
beginTime = time.time()
# 使用线程池并发处理
try:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_item = {
executor.submit(_process_single_item, v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): v
for v in listData
}
# 收集结果
success_count = 0
fail_count = 0
try:
for future in as_completed(future_to_item):
v = future_to_item[future]
try:
success, error_msg = future.result()
if success:
success_count += 1
log(f"处理成功: {v.get('file_name', 'unknown')} ({success_count}/{len(listData)})")
else:
fail_count += 1
log(f"处理失败: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})")
# 如果任何一个任务失败,记录错误但继续处理其他任务
except KeyboardInterrupt:
# 收到中断信号,取消所有未完成的任务
log(f"收到中断信号,正在取消未完成的任务...")
for f in future_to_item:
f.cancel()
raise # 重新抛出,让外层捕获
except Exception as e:
fail_count += 1
error_msg = f"处理数据项时发生异常: {str(e)}"
log(f"处理异常: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})")
except KeyboardInterrupt:
# 收到中断信号,取消所有未完成的任务
log(f"收到中断信号,正在取消未完成的任务...")
for f in future_to_item:
f.cancel()
# 尝试等待正在执行的任务完成(非阻塞方式)
import concurrent.futures
for f in list(future_to_item.keys()):
if not f.done():
try:
# 尝试获取结果,如果任务还在执行则立即返回
f.result(timeout=0.1)
except (concurrent.futures.TimeoutError, concurrent.futures.CancelledError):
pass
except:
pass
raise # 重新抛出,让外层捕获
endTime = time.time()
log(f"多线程处理完成, 总耗时{endTime - beginTime}秒, 成功:{success_count}, 失败:{fail_count}, 总计:{len(listData)}")
# 如果有任何失败,返回 False
if fail_count > 0:
log(f"部分任务处理失败,共{fail_count}个失败")
return False
return True
except KeyboardInterrupt:
log(f"多线程处理被中断")
raise # 重新抛出,让调用者处理
def findBpyModule():
# 返回 Blender 可执行文件路径
blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender'
# 判断当前是 windows 还是 macOS
if platform.system() == 'Windows':
# Windows 上常见的 Blender 安装路径
possible_paths = [
'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 4.4\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 4.3\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 4.2\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 4.1\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 4.0\\blender.exe',
]
# 查找存在的路径
for path in possible_paths:
if os.path.exists(path):
blender_bin_path = path
break
else:
# 如果都没找到,使用默认路径
blender_bin_path = 'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe'
log(f"警告: 未找到 Blender 可执行文件,使用默认路径: {blender_bin_path}")
else:
blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender'
# 检查路径是否存在
if not os.path.exists(blender_bin_path):
error_msg = f"Blender 可执行文件不存在: {blender_bin_path}"
log(error_msg)
raise FileNotFoundError(error_msg)
return blender_bin_path
-4
View File
@@ -1,4 +0,0 @@
import time
def log(message):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
@@ -1,45 +0,0 @@
import os
from .oss_redis import ossClient
from .logs import log
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
def download_file_with_check(ossFilePath, localFilePath):
try:
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
if not checkFileExists(ossFilePath):
log(f"文件不存在: {ossFilePath}")
return False
# 同步阻塞下载,下载完成后才会返回
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
# 1. 检查 HTTP 状态码
status = getattr(result, "status", None)
if status != 200:
log(f"下载失败,HTTP 状态码异常: status={status}")
return False
# 2. 远端 / 本地文件大小对比,作为二次校验
remote_meta = ossClient().head_object(ossFilePath)
remote_size = getattr(remote_meta, "content_length", None)
if remote_size is None:
log("无法获取远端文件大小,放弃本次下载结果")
return False
if not os.path.exists(localFilePath):
log("本地文件不存在,下载可能失败")
return False
local_size = os.path.getsize(localFilePath)
if remote_size != local_size:
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
return False
log("文件下载成功且完整性校验通过")
return True
except Exception as e:
log(f"下载文件出现异常: {str(e)}")
return False
def checkFileExists(ossFilePath):
return ossClient().object_exists(ossFilePath)
@@ -1,125 +0,0 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': 'mp.api.suwa3d.com',
'password': 'kcV2000',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
def redisClient():
"""获取Redis客户端单例"""
return RedisClientSingleton().get_client()
@@ -1,193 +0,0 @@
import os,time,sys,argparse
import numpy as np
import json
import warnings
def findBpyModule():
# /Applications/Blender.app/Contents/Resources/4.4/python/bin
blender_bin_path = '/Applications/Blender.app/Contents/Resources/4.4/python/bin/blender'
return blender_bin_path
def custom_mesh_transform(vertices, transform_matrix):
"""
手动实现网格变换:对每个顶点应用齐次变换矩阵
参数:
vertices: 网格顶点数组 (N, 3)
transform_matrix: 4x4 齐次变换矩阵
返回:
变换后的顶点数组 (N, 3)
"""
# 验证变换矩阵
transform_matrix = np.array(transform_matrix, dtype=np.float64)
# 检查矩阵形状
if transform_matrix.shape != (4, 4):
raise ValueError(f"变换矩阵必须是 4x4 矩阵,当前形状: {transform_matrix.shape}")
# 检查矩阵中是否包含无效值(NaN 或 Inf)
if np.any(np.isnan(transform_matrix)) or np.any(np.isinf(transform_matrix)):
invalid_count = np.sum(np.isnan(transform_matrix)) + np.sum(np.isinf(transform_matrix))
raise ValueError(f"变换矩阵包含无效值 (NaN/Inf): {invalid_count} 个无效元素\n矩阵:\n{transform_matrix}")
# 检查矩阵行列式(对于齐次变换矩阵,最后一行应该是 [0, 0, 0, 1])
# 如果矩阵接近奇异,行列式会接近0
det = np.linalg.det(transform_matrix[:3, :3]) # 只检查3x3旋转/缩放部分
if abs(det) < 1e-10:
print(f"警告: 变换矩阵的3x3部分行列式接近0 (det={det:.2e}),可能导致数值不稳定")
# 检查最后一行是否为 [0, 0, 0, 1](齐次变换矩阵的标准形式)
last_row = transform_matrix[3, :]
if not np.allclose(last_row, [0, 0, 0, 1], atol=1e-6):
print(f"警告: 变换矩阵最后一行不是标准形式 [0,0,0,1],而是: {last_row}")
# 1. 顶点转齐次坐标 (N, 3) → (N, 4)
homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1))))
# 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN)
# 捕获警告并检查结果
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always") # 捕获所有警告
transformed_homogeneous = transform_matrix @ homogeneous_vertices.T
# 如果有警告,记录详细信息
if w:
warning_msgs = [str(warning.message) for warning in w]
print(f"警告: 矩阵乘法过程中出现 {len(w)} 个警告:")
for msg in warning_msgs:
print(f" - {msg}")
print(f"变换矩阵:\n{transform_matrix}")
print(f"矩阵包含 NaN: {np.any(np.isnan(transform_matrix))}")
print(f"矩阵包含 Inf: {np.any(np.isinf(transform_matrix))}")
if not np.any(np.isnan(transform_matrix)) and not np.any(np.isinf(transform_matrix)):
print(f"矩阵最大值: {np.nanmax(transform_matrix):.6e}")
print(f"矩阵最小值: {np.nanmin(transform_matrix):.6e}")
# 检查结果是否包含无效值
if np.any(np.isnan(transformed_homogeneous)) or np.any(np.isinf(transformed_homogeneous)):
invalid_count = np.sum(np.isnan(transformed_homogeneous)) + np.sum(np.isinf(transformed_homogeneous))
raise ValueError(f"变换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素")
# 3. 转回非齐次坐标 (3xN) → (N, 3)
transformed_vertices = transformed_homogeneous[:3, :].T
return transformed_vertices
# {
# "file_name": "888687_324912_P117183_13.8cm_x1.obj",
# "transform": {
# "homo_matrix": [
# [-0.09437,0.995195,0.026116,77.975499],
# [-0.58016,-0.033658,-0.813807,322.60588],
# [-0.809017,-0.09195,0.580549,24.503686],
# [0.0,0.0,0.0,1.0]
# ]
# }
# },
def transform_save(obj_path,homo_matrix):
import bpy
# 延迟导入 bpy,只在 Blender 环境中可用
# try:
# blender_bin_path = findBpyModule()
# subprocess.run([blender_bin_path, "--background", "--python-expr", "import bpy"])
# except ImportError:
# log("错误: bpy 模块不可用。此函数只能在 Blender 环境中运行。")
# log("请使用 Blender 的 Python 解释器运行此脚本,或通过 Blender 命令行运行。")
# raise ImportError("bpy 模块不可用。此函数只能在 Blender 环境中运行。")
print("开始转换数据")
obj_name = obj_path.split("/")[-1]
print(f"obj_name={obj_name}")
# 清除场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
## 尺寸调整,环境设置
bpy.ops.object.delete(use_global=False, confirm=False)
bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS'
bpy.context.scene.unit_settings.scale_length = 0.001
bpy.context.scene.unit_settings.mass_unit = 'GRAMS'
reconstructed_matrix = np.array(homo_matrix, dtype=np.float64)
bpy.ops.wm.obj_import(filepath=obj_path)
imported_object = bpy.context.object
if imported_object is None or imported_object.type != 'MESH':
print(f"警告: 未能成功导入网格对象 {obj_name}。跳过。")
return
mesh_data = imported_object.data
mesh_data.calc_loop_triangles()
original_vertices = np.empty(len(mesh_data.vertices) * 3, dtype=np.float64)
mesh_data.vertices.foreach_get('co', original_vertices)
original_vertices = original_vertices.reshape(-1, 3)
try:
transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix)
# 验证转换后的顶点是否有效
if np.any(np.isnan(transformed_vertices)) or np.any(np.isinf(transformed_vertices)):
invalid_count = np.sum(np.isnan(transformed_vertices)) + np.sum(np.isinf(transformed_vertices))
error_msg = f"错误: 转换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素, obj_name={obj_name}"
print(error_msg)
print(f"变换矩阵:\n{reconstructed_matrix}")
raise ValueError(error_msg)
print(f"转换成功: {len(transformed_vertices)} 个顶点")
print(f"顶点坐标范围: X[{transformed_vertices[:, 0].min():.2f}, {transformed_vertices[:, 0].max():.2f}], "
f"Y[{transformed_vertices[:, 1].min():.2f}, {transformed_vertices[:, 1].max():.2f}], "
f"Z[{transformed_vertices[:, 2].min():.2f}, {transformed_vertices[:, 2].max():.2f}]")
except Exception as e:
error_msg = f"转换顶点时发生错误, obj_name={obj_name}, error={str(e)}"
print(error_msg)
print(f"变换矩阵:\n{reconstructed_matrix}")
raise
offset = np.array([-380, -345, 0])
transformed_vertices += offset
print(f"已对模型 {obj_name} 应用偏移: {offset}")
flattened_verts = transformed_vertices.reshape(-1)
mesh_data.vertices.foreach_set('co', flattened_verts)
mesh_data.update()
# meshes.append(imported_object)
# 导出前确保选中当前对象
bpy.ops.object.select_all(action='DESELECT')
imported_object.select_set(True)
bpy.context.view_layer.objects.active = imported_object
bpy.ops.wm.obj_export(
filepath=obj_path,
export_selected_objects=True,
export_materials=True,
path_mode='AUTO'
)
#os.remove(old_mtl_path)
if __name__ == "__main__":
script_args = []
try:
separator_index = sys.argv.index("--")
script_args = sys.argv[separator_index + 1:]
except ValueError:
pass
parser = argparse.ArgumentParser()
parser.add_argument("--objPathName", type=str, required=True, help="obj文件路径")
parser.add_argument("--trans", type=str, required=True, help="旋转参数(JSON格式)")
args = parser.parse_args(script_args)
print(f"objPathName={args.objPathName}, trans={args.trans}")
# 解析 JSON 字符串为列表
try:
homo_matrix = json.loads(args.trans)
except json.JSONDecodeError as e:
print(f"错误: 无法解析 trans 参数为 JSON: {e}")
sys.exit(1)
print("进入转换方法")
transform_save(args.objPathName, homo_matrix)
+10 -51
View File
@@ -1,54 +1,13 @@
from pathlib import Path
import os
import shutil
# 指定要遍历的文件夹路径
folder_path = Path('G://obj_fix/print')
import requests,json,os
# 定义函数来递归遍历文件夹下的所有文件
def list_files(directory):
for root, dirs, files in os.walk(directory):
for file in files:
yield os.path.join(root, file)
objFilePath = "/Users/dcx/code/make2/script/factory_sliceing/batchPrint/45/data"
objCounts = 0
for file in os.listdir(objFilePath):
print(file)
# if file == 'arrange':
# continue
if file.endswith('.obj'):
objCounts += 1
# 指定要遍历的文件夹路径
# folder_path = folder_path
# 使用list_files函数来列出文件夹下的所有文件
nums = 0
for file_path in list_files(folder_path):
if "around" in file_path:
continue
if "_new.obj" not in file_path:
continue
#提取pid
arrTemp = file_path.split('\\')
pid = arrTemp[len(arrTemp)-2]
#判段是否存在 {pid}.obj 文件,存在的话就进行重命名
initObjPath = f'G://obj_fix/print/{pid}/{pid}.obj'
if not os.path.exists(initObjPath):
continue
#存在的话就重命名文件
old_file_path = f'G://obj_fix/print/{pid}/{pid}_old.obj'
os.rename(initObjPath, old_file_path)
newObjfilePath = f'G://obj_fix/print/{pid}/{pid}_new.obj'
os.rename(newObjfilePath, initObjPath)
# # 将 new.obj 文件复制到指定的文件夹下 G:\obj_fix\print\{pid}\
# source_obj = file_path
# target_obj = f'G://obj_fix/print/{pid}/{pid}_new.obj'
# print(f"当前正在处理{pid}")
# shutil.copy(source_obj, target_obj)
nums += 1
print(pid,file_path,nums)
print(objCounts)
+21 -30
View File
@@ -1,33 +1,24 @@
import os
import shutil
import requests
folder_path = 'G:\\obj_fix\print'
#向 https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05
# 上传图片
def upload_image(image_path):
url = 'https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05'
with open(image_path, 'rb') as f:
image_data = f.read()
headers = {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': 'image/jpeg',
}
response = requests.put(url, data=image_data, headers=headers)
try:
response.raise_for_status()
except requests.HTTPError as e:
raise RuntimeError(f'上传失败 {response.status_code}: {response.text}') from e
return {
'status_code': response.status_code,
'etag': response.headers.get('ETag'),
}
# 遍历文件夹下的所有文件
# 定义函数来递归遍历文件夹下的所有文件
def list_files(directory):
for root, dirs, files in os.walk(directory):
for file in files:
yield os.path.join(root, file)
nums = 0
for file_path in list_files(folder_path):
if "_preview.png" not in file_path:
continue
print(file_path)
nums += 1
# #提取pid
# arrTemp = file_path.split('\\')
# pid = arrTemp[len(arrTemp)-2]
# #判断 G:\obj_fix\print\wrong\ 文件夹里 是否存在 {pid}_preview.png 文件
# filePath = f'G://obj_fix\print\wrong\{pid}_preview.png'
# #不存在跳过
# if not os.path.exists(filePath):
# continue
# #存在的话就重命名文件
# new_file_path = f'G://obj_fix\print\wrong\{pid}_preview_already_right.png'
# os.rename(filePath, new_file_path)
print(nums)
upload_image('273628Tex1.jpg')