460 lines
21 KiB
Python
460 lines
21 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Geometry UI Module for Plugin
|
||
几何模型UI模块 - 负责显示几何模型编辑界面和基础操作
|
||
基本功能:
|
||
- 模型拾取以及加载
|
||
- LOD模型分级过滤
|
||
- LOD模型创建
|
||
- 自动加载模型
|
||
- 标准化命名
|
||
- 自动分组
|
||
- 生成面部配件(睑毛,舌头,泪腺 等)
|
||
- 修复接缝(修复法线)
|
||
- 修复点序
|
||
"""
|
||
#========================================= IMPORT =========================================
|
||
from Qt import QtWidgets, QtCore, QtGui
|
||
from Qt.QtCompat import wrapInstance
|
||
from maya import OpenMayaUI as omui
|
||
import maya.cmds as cmds
|
||
import maya.mel as mel
|
||
import maya.utils as utils
|
||
import webbrowser
|
||
import subprocess
|
||
import importlib
|
||
import traceback
|
||
import locale
|
||
import sys
|
||
import os
|
||
from scripts.ui import ui_utils
|
||
from scripts.utils import utils_geometry
|
||
#========================================== CONFIG ========================================
|
||
import config
|
||
TOOL_NAME = config.TOOL_NAME
|
||
TOOL_VERSION = config.TOOL_VERSION
|
||
TOOL_AUTHOR = config.TOOL_AUTHOR
|
||
TOOL_YEAR = config.TOOL_YEAR
|
||
TOOL_MOD_FILENAME = config.TOOL_MOD_FILENAME
|
||
TOOL_LANG = config.TOOL_LANG
|
||
TOOL_WSCL_NAME = config.TOOL_WSCL_NAME
|
||
TOOL_HELP_URL = config.TOOL_HELP_URL
|
||
TOOL_PATH = config.TOOL_PATH
|
||
SCRIPTS_PATH = config.SCRIPTS_PATH
|
||
TOOL_MAIN_SCRIPT = config.TOOL_MAIN_SCRIPT
|
||
UI_PATH = config.UI_PATH
|
||
STYLE_FILE = config.STYLE_FILE
|
||
ICONS_PATH = config.ICONS_PATH
|
||
TOOL_ICON = config.TOOL_ICON
|
||
ASSETS_PATH = config.ASSETS_PATH
|
||
DNA_FILE_PATH = config.DNA_FILE_PATH
|
||
DNA_IMG_PATH = config.DNA_IMG_PATH
|
||
TOOL_COMMAND_ICON = config.TOOL_COMMAND_ICON
|
||
TOOL_WIDTH = config.TOOL_WIDTH
|
||
TOOL_HEIGHT = config.TOOL_HEIGHT
|
||
#========================================= LOCATION =======================================
|
||
from scripts.ui import localization
|
||
LANG = localization.LANG
|
||
|
||
class GeometryUI(ui_utils.BaseUI):
|
||
"""
|
||
几何模型UI类 - 负责显示几何模型编辑界面和基础操作
|
||
继承自BaseUI类,实现几何模型相关的UI功能
|
||
"""
|
||
#========================================== INIT ========================================
|
||
def __init__(self):
|
||
"""
|
||
初始化几何模型UI
|
||
创建主控件和布局,并连接信号和槽
|
||
"""
|
||
super(GeometryUI, self).__init__()
|
||
|
||
# 创建主控件
|
||
self.main_widget = QtWidgets.QWidget()
|
||
self.main_widget.setObjectName("geometryMainWidget")
|
||
|
||
# 初始化UI
|
||
self.create_widgets()
|
||
self.create_layouts()
|
||
self.create_connections()
|
||
|
||
#========================================= WIDGET =======================================
|
||
def create_widgets(self):
|
||
"""
|
||
创建几何模型UI控件
|
||
包括按钮、标签、列表等
|
||
"""
|
||
# 标题标签 - 使用HTML格式化标题
|
||
title_text = f"<h4 style='margin:0;padding:5px;'>{LANG.get('geometry_title', '几何模型')}</h4>"
|
||
self.controls["title_label"] = QtWidgets.QLabel(title_text)
|
||
self.controls["title_label"].setObjectName("geometryTitleLabel")
|
||
self.controls["title_label"].setAlignment(QtCore.Qt.AlignCenter)
|
||
self.controls["title_label"].setMaximumHeight(30) # 限制标题高度
|
||
|
||
# 创建主容器
|
||
self.controls["main_container"] = QtWidgets.QWidget()
|
||
self.controls["main_container"].setObjectName("geometryMainContainer")
|
||
|
||
# 创建标签页控件
|
||
self.controls["tab_widget"] = QtWidgets.QTabWidget()
|
||
self.controls["tab_widget"].setObjectName("geometryTabWidget")
|
||
|
||
# 创建LOD标签页
|
||
self.create_lod_tabs()
|
||
|
||
# 创建底部功能按钮区域
|
||
self.create_bottom_buttons()
|
||
|
||
# 模型列表
|
||
self.controls["model_list"] = QtWidgets.QListWidget()
|
||
self.controls["model_list"].setObjectName("modelList")
|
||
|
||
# 模型操作按钮
|
||
self.buttons["add_model"] = QtWidgets.QPushButton(LANG.get("add_model", "添加模型"))
|
||
self.buttons["add_model"].setObjectName("addModelButton")
|
||
|
||
self.buttons["remove_model"] = QtWidgets.QPushButton(LANG.get("remove_model", "移除模型"))
|
||
self.buttons["remove_model"].setObjectName("removeModelButton")
|
||
|
||
self.buttons["duplicate_model"] = QtWidgets.QPushButton(LANG.get("duplicate_model", "复制模型"))
|
||
self.buttons["duplicate_model"].setObjectName("duplicateModelButton")
|
||
|
||
# 右侧面板控件 - 模型属性
|
||
self.controls["model_properties_group"] = QtWidgets.QGroupBox(LANG.get("model_properties", "模型属性"))
|
||
self.controls["model_properties_group"].setObjectName("modelPropertiesGroup")
|
||
|
||
# 模型名称标签和输入框
|
||
self.controls["model_name_label"] = QtWidgets.QLabel(LANG.get("name", "名称:"))
|
||
self.controls["model_name_label"].setObjectName("modelNameLabel")
|
||
|
||
self.controls["model_name_input"] = QtWidgets.QLineEdit()
|
||
self.controls["model_name_input"].setObjectName("modelNameInput")
|
||
self.controls["model_name_input"].setPlaceholderText(LANG.get("enter_model_name", "输入模型名称"))
|
||
|
||
# 模型类型标签和下拉框
|
||
self.controls["model_type_label"] = QtWidgets.QLabel(LANG.get("type", "类型:"))
|
||
self.controls["model_type_label"].setObjectName("modelTypeLabel")
|
||
|
||
self.controls["model_type_combo"] = QtWidgets.QComboBox()
|
||
self.controls["model_type_combo"].setObjectName("modelTypeCombo")
|
||
self.controls["model_type_combo"].addItems(["Base", "Head", "Body", "Accessory", "Other"])
|
||
|
||
# 模型可见性复选框
|
||
self.controls["model_visible_check"] = QtWidgets.QCheckBox(LANG.get("visible", "可见"))
|
||
self.controls["model_visible_check"].setObjectName("modelVisibleCheck")
|
||
self.controls["model_visible_check"].setChecked(True)
|
||
|
||
# 模型属性按钮
|
||
self.buttons["apply_properties"] = QtWidgets.QPushButton(LANG.get("apply", "应用"))
|
||
self.buttons["apply_properties"].setObjectName("applyPropertiesButton")
|
||
|
||
self.buttons["reset_properties"] = QtWidgets.QPushButton(LANG.get("reset", "重置"))
|
||
self.buttons["reset_properties"].setObjectName("resetPropertiesButton")
|
||
|
||
# 右侧面板控件 - 模型工具
|
||
self.controls["model_tools_group"] = QtWidgets.QGroupBox(LANG.get("model_tools", "模型工具"))
|
||
self.controls["model_tools_group"].setObjectName("modelToolsGroup")
|
||
|
||
# 模型工具按钮
|
||
self.buttons["standardize_names"] = QtWidgets.QPushButton(LANG.get("standardize_names", "标准化命名"))
|
||
self.buttons["standardize_names"].setObjectName("standardizeNamesButton")
|
||
|
||
self.buttons["auto_group"] = QtWidgets.QPushButton(LANG.get("auto_group", "自动分组"))
|
||
self.buttons["auto_group"].setObjectName("autoGroupButton")
|
||
|
||
self.buttons["generate_accessories"] = QtWidgets.QPushButton(LANG.get("generate_accessories", "生成配件"))
|
||
self.buttons["generate_accessories"].setObjectName("generateAccessoriesButton")
|
||
|
||
self.buttons["fix_seams"] = QtWidgets.QPushButton(LANG.get("fix_seams", "修复接缝"))
|
||
self.buttons["fix_seams"].setObjectName("fixSeamsButton")
|
||
|
||
self.buttons["fix_vertex_order"] = QtWidgets.QPushButton(LANG.get("fix_vertex_order", "修复点序"))
|
||
self.buttons["fix_vertex_order"].setObjectName("fixVertexOrderButton")
|
||
|
||
# 底部工具面板
|
||
# 导入部分
|
||
self.controls["import_group"] = QtWidgets.QGroupBox(LANG.get("import", "导入"))
|
||
self.controls["import_group"].setObjectName("importGroup")
|
||
|
||
self.buttons["import_model"] = QtWidgets.QPushButton(LANG.get("import_model", "导入模型"))
|
||
self.buttons["import_model"].setObjectName("importModelButton")
|
||
|
||
self.buttons["import_fbx"] = QtWidgets.QPushButton(LANG.get("import_fbx", "导入FBX"))
|
||
self.buttons["import_fbx"].setObjectName("importFbxButton")
|
||
|
||
self.buttons["import_obj"] = QtWidgets.QPushButton(LANG.get("import_obj", "导入OBJ"))
|
||
self.buttons["import_obj"].setObjectName("importObjButton")
|
||
|
||
# 导出部分
|
||
self.controls["export_group"] = QtWidgets.QGroupBox(LANG.get("export", "导出"))
|
||
self.controls["export_group"].setObjectName("exportGroup")
|
||
|
||
self.buttons["export_model"] = QtWidgets.QPushButton(LANG.get("export_model", "导出模型"))
|
||
self.buttons["export_model"].setObjectName("exportModelButton")
|
||
|
||
self.buttons["export_fbx"] = QtWidgets.QPushButton(LANG.get("export_fbx", "导出 FBX"))
|
||
self.buttons["export_fbx"].setObjectName("exportFbxButton")
|
||
|
||
self.buttons["export_obj"] = QtWidgets.QPushButton(LANG.get("export_obj", "导出 OBJ"))
|
||
self.buttons["export_obj"].setObjectName("exportObjButton")
|
||
|
||
# 工具部分
|
||
self.controls["tools_group"] = QtWidgets.QGroupBox(LANG.get("tools", "工具"))
|
||
self.controls["tools_group"].setObjectName("toolsGroup")
|
||
|
||
self.buttons["check_model"] = QtWidgets.QPushButton(LANG.get("check_model", "检查模型"))
|
||
self.buttons["check_model"].setObjectName("checkModelButton")
|
||
|
||
self.buttons["optimize_model"] = QtWidgets.QPushButton(LANG.get("optimize_model", "优化模型"))
|
||
self.buttons["optimize_model"].setObjectName("optimizeModelButton")
|
||
|
||
self.buttons["clean_model"] = QtWidgets.QPushButton(LANG.get("clean_model", "清理模型"))
|
||
self.buttons["clean_model"].setObjectName("cleanModelButton")
|
||
|
||
self.buttons["uv_tools"] = QtWidgets.QPushButton(LANG.get("uv_tools", "UV工具"))
|
||
self.buttons["uv_tools"].setObjectName("uvToolsButton")
|
||
|
||
def create_lod_tabs(self):
|
||
"""
|
||
创建LOD标签页
|
||
包括LOD0~LOD7的标签页
|
||
"""
|
||
# 创建8个LOD标签页
|
||
lod_names = [f"LOD{i}" for i in range(8)]
|
||
|
||
# 创建标签页和内容
|
||
for lod_name in lod_names:
|
||
# 创建标签页
|
||
tab = QtWidgets.QWidget()
|
||
tab.setObjectName(f"{lod_name}Tab")
|
||
|
||
# 创建标签页布局
|
||
tab_layout = QtWidgets.QVBoxLayout(tab)
|
||
tab_layout.setContentsMargins(5, 5, 5, 5)
|
||
tab_layout.setSpacing(5)
|
||
|
||
# 创建身体部位输入字段
|
||
body_parts = [
|
||
"头部", "牙齿", "牙龈", "左眼", "右眼", "红脸", "眉毛", "眉毛", "躯体"
|
||
]
|
||
|
||
# 根据LOD级别限制显示的身体部位
|
||
if lod_name in ["LOD0", "LOD1"]:
|
||
parts_to_show = body_parts
|
||
elif lod_name in ["LOD2", "LOD3"]:
|
||
parts_to_show = body_parts[:8]
|
||
elif lod_name in ["LOD4", "LOD5"]:
|
||
parts_to_show = body_parts[:5]
|
||
else: # LOD6, LOD7
|
||
parts_to_show = body_parts[:4]
|
||
|
||
# 为每个身体部位创建输入字段和加载按钮
|
||
for part in parts_to_show:
|
||
# 创建水平布局
|
||
part_layout = QtWidgets.QHBoxLayout()
|
||
part_layout.setSpacing(5)
|
||
|
||
# 创建标签
|
||
label = QtWidgets.QLabel(f"{part}:")
|
||
label.setMinimumWidth(40)
|
||
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||
|
||
# 创建输入字段
|
||
input_field = QtWidgets.QLineEdit()
|
||
input_field.setObjectName(f"{lod_name}_{part}_input")
|
||
input_field.setPlaceholderText(LANG.get("enter_model_name", "输入模型名称"))
|
||
|
||
# 创建加载按钮
|
||
load_button = QtWidgets.QPushButton(LANG.get(" load ", " 加 载 "))
|
||
load_button.setObjectName(f"{lod_name}_{part}_load_button")
|
||
load_button.setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, "loading.png")))
|
||
load_button.setFixedSize(120, 24)
|
||
load_button.setToolTip(LANG.get("load_model", "加载模型"))
|
||
|
||
# 将控件添加到布局
|
||
part_layout.addWidget(label)
|
||
part_layout.addWidget(input_field)
|
||
part_layout.addWidget(load_button)
|
||
|
||
# 将布局添加到标签页布局
|
||
tab_layout.addLayout(part_layout)
|
||
|
||
# 保存控件引用
|
||
self.controls[f"{lod_name}_{part}_label"] = label
|
||
self.controls[f"{lod_name}_{part}_input"] = input_field
|
||
self.buttons[f"{lod_name}_{part}_load"] = load_button
|
||
|
||
# 添加弹性空间
|
||
tab_layout.addStretch(1)
|
||
|
||
# 将标签页添加到标签页控件
|
||
self.controls["tab_widget"].addTab(tab, lod_name)
|
||
|
||
# 添加垃圾桶按钮到标签页控件右侧
|
||
trash_icon = QtGui.QIcon(os.path.join(ICONS_PATH, "delete.png"))
|
||
trash_button = QtWidgets.QPushButton()
|
||
trash_button.setIcon(trash_icon)
|
||
trash_button.setIconSize(QtCore.QSize(28, 28))
|
||
trash_button.setFixedSize(28, 28)
|
||
trash_button.setToolTip(LANG.get("delete", "删除"))
|
||
trash_button.setStyleSheet("QPushButton { border: none; background-color: transparent; }")
|
||
self.controls["tab_widget"].setCornerWidget(trash_button, QtCore.Qt.TopRightCorner)
|
||
self.buttons["delete"] = trash_button
|
||
|
||
def create_bottom_buttons(self):
|
||
"""
|
||
创建底部功能按钮区域
|
||
包括模型工具组,其中包含拓扑结构、选择LOD、创建LOD按钮等
|
||
"""
|
||
# 创建底部功能区域
|
||
self.controls["bottom_area"] = QtWidgets.QWidget()
|
||
self.controls["bottom_area"].setObjectName("geometryBottomArea")
|
||
self.controls["bottom_area"].setMinimumHeight(180)
|
||
self.controls["bottom_area"].setMaximumHeight(250)
|
||
|
||
# 创建底部区域布局
|
||
bottom_layout = QtWidgets.QVBoxLayout(self.controls["bottom_area"])
|
||
bottom_layout.setContentsMargins(5, 5, 5, 5)
|
||
bottom_layout.setSpacing(5)
|
||
|
||
# 创建模型工具区域
|
||
model_tools_group = QtWidgets.QGroupBox(LANG.get("model_tools", "模型工具"))
|
||
model_tools_group.setObjectName("modelToolsGroup")
|
||
|
||
# 创建模型工具布局
|
||
model_tools_layout = QtWidgets.QGridLayout(model_tools_group)
|
||
model_tools_layout.setContentsMargins(5, 5, 5, 5)
|
||
model_tools_layout.setSpacing(5)
|
||
|
||
# 第一行放置拓扑结构、选择LOD和创建LOD按钮
|
||
# 拓扑结构标签和下拉框
|
||
topology_label = QtWidgets.QLabel(LANG.get("topology_structure", "拓扑结构") + ":")
|
||
model_tools_layout.addWidget(topology_label, 0, 0)
|
||
|
||
self.controls["topology_combo"] = QtWidgets.QComboBox()
|
||
self.controls["topology_combo"].setObjectName("topologyCombo")
|
||
self.controls["topology_combo"].addItem("MetaHuman")
|
||
self.controls["topology_combo"].setMinimumWidth(100)
|
||
model_tools_layout.addWidget(self.controls["topology_combo"], 0, 1)
|
||
|
||
# 选择LOD标签和下拉框
|
||
lod_label = QtWidgets.QLabel(LANG.get("select_lod", "选择LOD") + ":")
|
||
model_tools_layout.addWidget(lod_label, 0, 2)
|
||
|
||
self.controls["lod_combo"] = QtWidgets.QComboBox()
|
||
self.controls["lod_combo"].setObjectName("lodCombo")
|
||
self.controls["lod_combo"].addItem(LANG.get("all", "全部"))
|
||
for i in range(8): # LOD0~LOD7
|
||
self.controls["lod_combo"].addItem(f"LOD{i}")
|
||
self.controls["lod_combo"].setMinimumWidth(80)
|
||
model_tools_layout.addWidget(self.controls["lod_combo"], 0, 3)
|
||
|
||
# 创建LOD按钮
|
||
self.buttons["create_lod"] = QtWidgets.QPushButton()
|
||
self.buttons["create_lod"].setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, "create_lod.png")))
|
||
self.buttons["create_lod"].setText(LANG.get("create_lod", "创建LOD"))
|
||
self.buttons["create_lod"].setObjectName("createLodButton")
|
||
self.buttons["create_lod"].setMinimumHeight(30)
|
||
model_tools_layout.addWidget(self.buttons["create_lod"], 0, 4)
|
||
|
||
# 创建模型工具按钮
|
||
tool_buttons = [
|
||
{"name": "separate_model", "text": LANG.get("separate_model", "模型分离"), "icon": "polySplitVertex.png ", "row": 1, "col": 0, "colspan": 2},
|
||
{"name": "generate_face_components", "text": LANG.get("generate_face_components", "生成面部配件"), "icon": "meshes.png", "row": 1, "col": 2, "colspan": 3},
|
||
{"name": "fix_normals", "text": LANG.get("fix_normals", "修复法线"), "icon": "fix_normals.png", "row": 2, "col": 0, "colspan": 2},
|
||
{"name": "fix_vertex_order", "text": LANG.get("fix_vertex_order", "修复点序"), "icon": "normalConstraint.png ", "row": 2, "col": 2, "colspan": 3},
|
||
{"name": "fix_seams", "text": LANG.get("fix_seams", "修复接缝"), "icon": "polyChipOff.png ", "row": 3, "col": 0, "colspan": 2},
|
||
{"name": "optimize_scene", "text": LANG.get("optimize_scene", "优化场景"), "icon": "singlePerspLayout.png ", "row": 3, "col": 2, "colspan": 3}
|
||
]
|
||
|
||
# 创建并添加按钮到布局
|
||
for btn_info in tool_buttons:
|
||
button = QtWidgets.QPushButton()
|
||
button.setText(btn_info["text"])
|
||
button.setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, btn_info["icon"])))
|
||
button.setObjectName(f"{btn_info['name']}Button")
|
||
button.setMinimumHeight(30)
|
||
model_tools_layout.addWidget(button, btn_info["row"], btn_info["col"], 1, btn_info["colspan"])
|
||
self.buttons[btn_info["name"]] = button
|
||
|
||
# 添加布局到底部区域
|
||
bottom_layout.addWidget(model_tools_group)
|
||
|
||
#========================================= LAYOUT =======================================
|
||
def create_layouts(self):
|
||
"""
|
||
创建几何模型UI布局
|
||
组织控件的排列和层次结构
|
||
"""
|
||
# 主布局
|
||
self.layouts["main_layout"] = QtWidgets.QVBoxLayout(self.main_widget)
|
||
self.layouts["main_layout"].setContentsMargins(0, 0, 0, 0)
|
||
self.layouts["main_layout"].setSpacing(0)
|
||
|
||
# 添加标题标签
|
||
self.layouts["main_layout"].addWidget(self.controls["title_label"])
|
||
|
||
# 添加标签页控件到主布局
|
||
self.layouts["main_layout"].addWidget(self.controls["tab_widget"])
|
||
|
||
# 添加底部功能按钮区域到主布局
|
||
self.layouts["main_layout"].addWidget(self.controls["bottom_area"])
|
||
|
||
#======================================= CONNECTION =====================================
|
||
def create_connections(self):
|
||
"""
|
||
连接信号和槽
|
||
设置UI控件的交互行为
|
||
"""
|
||
# 导入几何工具函数
|
||
from scripts.utils import utils_geometry
|
||
|
||
# 连接LOD标签页的加载按钮
|
||
lod_names = [f"LOD{i}" for i in range(8)]
|
||
body_parts = [
|
||
"头部", "牙齿", "牙龈", "左眼", "右眼", "红脸", "眉毛", "眉毛", "躯体"
|
||
]
|
||
|
||
# 根据LOD级别限制连接的身体部位
|
||
for lod_name in lod_names:
|
||
if lod_name in ["LOD0", "LOD1"]:
|
||
parts_to_connect = body_parts
|
||
elif lod_name in ["LOD2", "LOD3"]:
|
||
parts_to_connect = body_parts[:8]
|
||
elif lod_name in ["LOD4", "LOD5"]:
|
||
parts_to_connect = body_parts[:5]
|
||
else: # LOD6, LOD7
|
||
parts_to_connect = body_parts[:4]
|
||
|
||
# 连接每个身体部位的加载按钮
|
||
for part in parts_to_connect:
|
||
# 创建一个闭包来保存当前的lod_name和part值
|
||
def create_load_callback(lod, part):
|
||
return lambda: utils_geometry.load_model_for_lod(lod, part)
|
||
|
||
# 连接加载按钮
|
||
self.buttons[f"{lod_name}_{part}_load"].clicked.connect(
|
||
create_load_callback(lod_name, part)
|
||
)
|
||
|
||
# 连接垃圾桶按钮
|
||
self.buttons["delete"].clicked.connect(utils_geometry.delete)
|
||
|
||
# 连接拓扑结构下拉框
|
||
self.controls["topology_combo"].currentIndexChanged.connect(utils_geometry.update_topology)
|
||
|
||
# 连接选择LOD下拉框
|
||
self.controls["lod_combo"].currentIndexChanged.connect(utils_geometry.select_lod)
|
||
|
||
# 连接创建LOD按钮
|
||
self.buttons["create_lod"].clicked.connect(utils_geometry.create_lod)
|
||
|
||
# 连接模型工具按钮
|
||
self.buttons["separate_model"].clicked.connect(utils_geometry.separate_model)
|
||
self.buttons["generate_face_components"].clicked.connect(utils_geometry.generate_face_components)
|
||
self.buttons["fix_normals"].clicked.connect(utils_geometry.fix_normals)
|
||
self.buttons["fix_vertex_order"].clicked.connect(utils_geometry.fix_vertex_order)
|
||
self.buttons["fix_seams"].clicked.connect(utils_geometry.fix_seams)
|
||
self.buttons["optimize_scene"].clicked.connect(utils_geometry.optimize_scene) |