from ngSkinTools2 import cleanup, signal from ngSkinTools2.api import influenceMapping, mirror from ngSkinTools2.api.log import getLogger from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets from ngSkinTools2.signal import Signal from ngSkinTools2.ui import dialogs, qt, widgets from ngSkinTools2.ui.dialogs import yesNo from ngSkinTools2.ui.layout import scale_multiplier from ngSkinTools2.ui.options import config from ngSkinTools2.ui.widgets import NumberSliderGroup log = getLogger("influence mapping UI") def open_ui_for_mesh(ui_parent, mesh): m = mirror.Mirror(mesh) mapper = m.build_influences_mapper() def do_apply(mapping): m.set_influences_mapping(mapping) m.save_influences_mapper(mapper) return open_as_dialog(ui_parent, mapper, do_apply) def open_as_dialog(parent, matcher, result_callback): """ :type matcher: ngSkinTools2.api.influenceMapping.InfluenceMapping """ main_layout, reload_ui, recalc_matches = build_ui(parent, matcher) def button_row(window): def apply(): result_callback(matcher.asIntIntMapping(matcher.calculatedMapping)) window.close() def save_defaults(): if not yesNo("将当前设置保存为默认设置?"): return config.mirrorInfluencesDefaults = matcher.config.as_json() def load_defaults(): matcher.config.load_json(config.mirrorInfluencesDefaults) reload_ui() recalc_matches() return widgets.button_row( [ ("应用", apply), ("取消", window.close), ], side_menu=[ ("储存为默认值", save_defaults), ("加载默认值", load_defaults), ], ) window = QtWidgets.QDialog(parent) cleanup.registerCleanupHandler(window.close) window.setWindowTitle("影响镜像映射") window.setAttribute(QtCore.Qt.WA_DeleteOnClose) window.resize(720 * scale_multiplier, 500 * scale_multiplier) window.setLayout(QtWidgets.QVBoxLayout()) window.layout().addWidget(main_layout) window.layout().addLayout(button_row(window)) window.show() recalc_matches() return window def build_ui(parent, matcher): """ :param parent: parent qt widget :type matcher: influenceMapping.InfluenceMapping """ influence_data = matcher.influences influenceMapping.calcShortestUniqueName(matcher.influences) if matcher.destinationInfluences is not None and matcher.destinationInfluences != matcher.influences: influenceMapping.calcShortestUniqueName(matcher.destinationInfluences) update_globs = Signal("need recalc") reload_ui = Signal("reload_ui") mirror_mode = matcher.config.mirror_axis is not None def build_tree_hierarchy(tree_view): tree_items = {} # mapping of path->treeItem influence_items = {} # same as above, only includes non-intermediate items def find_item(path, is_intermediate): result = tree_items.get(path, None) if result is not None: return result split_path = path.rsplit("|", 1) parent_path, name = split_path if len(split_path) == 2 else ["", split_path[0]] item = QtWidgets.QTreeWidgetItem([name, '-', '(不在皮肤簇中)' if is_intermediate else '?']) tree_items[path] = item parent_item = None if parent_path is "" else find_item(parent_path, True) if parent_item is not None: parent_item.addChild(item) else: tree_view.addTopLevelItem(item) item.setExpanded(True) return item for i in influence_data: influence_items[i.path_name()] = find_item(i.path_name(), False) return influence_items def tolerance(): result = NumberSliderGroup(min_value=0.001, max_value=10) result.spinner.setDecimals(3) @signal.on(reload_ui) def reload(): with qt.signals_blocked(result): result.set_value(matcher.config.distance_threshold) @signal.on(result.valueChanged) def changed(): matcher.config.distance_threshold = result.value() recalcMatches() reload() return result def pattern(): result = QtWidgets.QTableWidget() result.setColumnCount(2) result.setHorizontalHeaderLabels(["左", "右"] if mirror_mode else ["来源", "目标"]) result.setEditTriggers(QtWidgets.QTableWidget.AllEditTriggers) result.verticalHeader().setVisible(False) result.verticalHeader().setDefaultSectionSize(20) item_font = QtGui.QFont("Courier New", 12) item_font.setStyleHint(QtGui.QFont.Monospace) @signal.on(reload_ui) def reload_patterns(): with qt.signals_blocked(result): result.setRowCount(len(matcher.config.globs) + 1) for rowIndex, patterns in enumerate(matcher.config.globs + [('', '')]): for colIndex, p in enumerate(patterns): item = QtWidgets.QTableWidgetItem(p) item.setFont(item_font) result.setItem(rowIndex, colIndex, item) reload_patterns() @signal.on(update_globs) def update_matcher_globs(): globs = [] def text(r, c): item = result.item(r, c) if item is None: return "" return item.text().strip() for row in range(result.rowCount()): v1 = text(row, 0) v2 = text(row, 1) if v1 != "" and v2 != "": globs.append((v1, v2)) matcher.config.globs = globs recalcMatches() @qt.on(result.itemChanged) def item_changed(item): log.debug("item changed") item.setText(item.text().strip()) try: influenceMapping.validate_glob(item.text().strip()) except Exception as err: dialogs.displayError(str(err)) item.setText(influenceMapping.illegalCharactersRegexp.sub("", item.text())) if item.row() != result.rowCount() - 1: if item.text().strip() == "": result.removeRow(item.row()) # ensure one empty line at the end rows = result.rowCount() last_item = result.item(rows - 1, 0) if last_item and last_item.text() != "": result.setRowCount(rows + 1) update_matcher_globs() return result def automaticRules(): form = QtWidgets.QFormLayout() use_joint_names = QtWidgets.QCheckBox("匹配骨骼名称") naming_patterns = pattern() use_position = QtWidgets.QCheckBox("匹配位置") tolerance_scroll = tolerance() use_joint_labels = QtWidgets.QCheckBox("匹配骨骼标签") use_dg_links = QtWidgets.QCheckBox("匹配依存关系图连接") def update_enabled_disabled(): def enable_form_row(form_item, e): form_item.setEnabled(e) form.labelForField(form_item).setEnabled(e) checked = use_joint_names.isChecked() enable_form_row(naming_patterns, checked) checked = use_position.isChecked() tolerance_scroll.set_enabled(checked) form.labelForField(tolerance_scroll.layout()).setEnabled(checked) enable_form_row(dg_attribute, use_dg_links.isChecked()) @qt.on(use_joint_names.toggled, use_position.toggled, use_joint_labels.toggled, use_dg_links.toggled) def use_joint_names_toggled(): update_enabled_disabled() matcher.config.use_name_matching = use_joint_names.isChecked() matcher.config.use_distance_matching = use_position.isChecked() matcher.config.use_label_matching = use_joint_labels.isChecked() matcher.config.use_dg_link_matching = use_dg_links.isChecked() recalcMatches() dg_attribute = QtWidgets.QLineEdit() @qt.on(dg_attribute.editingFinished) def use_joint_names_toggled(): matcher.config.dg_destination_attribute = str(dg_attribute.text()).strip() recalcMatches() @signal.on(reload_ui) def update_values(): with qt.signals_blocked(dg_attribute): dg_attribute.setText(matcher.config.dg_destination_attribute) with qt.signals_blocked(use_joint_names): use_joint_names.setChecked(matcher.config.use_name_matching) with qt.signals_blocked(use_position): use_position.setChecked(matcher.config.use_distance_matching) with qt.signals_blocked(use_joint_labels): use_joint_labels.setChecked(matcher.config.use_label_matching) with qt.signals_blocked(use_dg_links): use_dg_links.setChecked(matcher.config.use_dg_link_matching) update_enabled_disabled() g = QtWidgets.QGroupBox("规则") g.setLayout(form) form.addRow(use_dg_links) form.addRow("属性名称:", dg_attribute) form.addRow(use_joint_labels) form.addRow(use_joint_names) form.addRow("命名方案:", naming_patterns) form.addRow(use_position) form.addRow("位置容差:", tolerance_scroll.layout()) update_values() return g def scriptedRules(): g = QtWidgets.QGroupBox("脚本规则") g.setLayout(QtWidgets.QVBoxLayout()) g.layout().addWidget(QtWidgets.QLabel("TODO")) return g def manualRules(): g = QtWidgets.QGroupBox("手动覆盖") g.setLayout(QtWidgets.QVBoxLayout()) g.layout().addWidget(QtWidgets.QLabel("TODO")) return g leftSide = QtWidgets.QScrollArea() leftSide.setFrameShape(QtWidgets.QFrame.NoFrame) leftSide.setFocusPolicy(QtCore.Qt.NoFocus) leftSide.setWidgetResizable(True) l = QtWidgets.QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.addWidget(automaticRules()) # l.addWidget(scriptedRules()) # l.addWidget(manualRules()) # l.addStretch() leftSide.setWidget(qt.wrap_layout_into_widget(l)) def createMappingView(): view = QtWidgets.QTreeWidget() view.setColumnCount(3) view.setHeaderLabels(["来源", "目标", "按规则匹配"]) view.setIndentation(7) view.setExpandsOnDoubleClick(False) usedItems = build_tree_hierarchy(view) linkedItemRole = QtCore.Qt.UserRole + 1 def previewMapping(mapping): """ :type mapping: dict[InfluenceInfo, InfluenceInfo] """ for treeItem in list(usedItems.values()): treeItem.setText(1, "(不匹配)") treeItem.setText(2, "") for k, v in list(mapping.items()): treeItem = usedItems.get(k.path_name(), None) if treeItem is None: continue treeItem.setText(1, "(自己)" if k == v['infl'] else v["infl"].shortestPath) treeItem.setText(2, v["matchedRule"]) treeItem.setData(1, linkedItemRole, v["infl"].path) @qt.on(view.itemDoubleClicked) def itemDoubleClicked(item, column): item.setExpanded(True) linkedItemPath = item.data(1, linkedItemRole) item = usedItems.get(linkedItemPath, None) if item is not None: item.setSelected(True) view.scrollToItem(item) return view, previewMapping def recalcMatches(): matches = matcher.calculate() mappingView_updateMatches(matches) g = QtWidgets.QGroupBox("计算映射") g.setLayout(QtWidgets.QVBoxLayout()) mappingView, mappingView_updateMatches = createMappingView() g.layout().addWidget(mappingView) mainLayout = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal, parent=parent) mainLayout.addWidget(leftSide) mainLayout.addWidget(g) mainLayout.setStretchFactor(0, 10) mainLayout.setStretchFactor(1, 10) mainLayout.setCollapsible(0, True) mainLayout.setSizes([200] * 2) return mainLayout, reload_ui.emit, recalcMatches