From 5c494307c136aed8dd724d38f5890842a68ffee6 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Mon, 22 Sep 2025 16:33:25 +0800 Subject: [PATCH 01/19] Add Fgui-Converter Entry Add a Qt Widget for Fgui-Convert, which can be found in Navigator window. But "Convert" Button has no function yet. --- .../forms/fguiconverterwidget.ui | 174 +++++++++++++ .../mainwindowinterface.py | 5 + src/preppipe_gui_pyside6/navigatorwidget.py | 2 + .../toolwidgets/fguiconverter.py | 244 ++++++++++++++++++ 4 files changed, 425 insertions(+) create mode 100644 src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui create mode 100644 src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py diff --git a/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui b/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui new file mode 100644 index 0000000..e62a247 --- /dev/null +++ b/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui @@ -0,0 +1,174 @@ + + + FguiConverterWidget + + + + 0 + 0 + 852 + 406 + + + + Form + + + + + 20 + 60 + 441 + 171 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOn + + + true + + + QAbstractItemView::DragDropMode::InternalMove + + + QAbstractItemView::SelectionBehavior::SelectRows + + + + + + 20 + 30 + 181 + 21 + + + + + 0 + 0 + + + + FGUI资源目录 + + + + + + 20 + 300 + 541 + 41 + + + + + + + true + + + (未指定) + + + + + + + 选择 + + + + + + + + + 610 + 60 + 121 + 301 + + + + 转换 + + + + + + 470 + 60 + 91 + 181 + + + + + + + 添加 + + + + + + + 删除 + + + + + + + 打开所在目录 + + + + + + + + + 20 + 250 + 541 + 20 + + + + Qt::Orientation::Horizontal + + + + + + 570 + 60 + 20 + 301 + + + + Qt::Orientation::Vertical + + + + + + 20 + 270 + 171 + 39 + + + + Ren'Py项目基目录 + + + + + + diff --git a/src/preppipe_gui_pyside6/mainwindowinterface.py b/src/preppipe_gui_pyside6/mainwindowinterface.py index 876caab..1efd657 100644 --- a/src/preppipe_gui_pyside6/mainwindowinterface.py +++ b/src/preppipe_gui_pyside6/mainwindowinterface.py @@ -61,3 +61,8 @@ def handleLanguageChange(self) -> None: zh_cn="WebGal 导出", zh_hk="WebGal 導出", ) + tr_toolname_guiconverter = TR_gui_mainwindow.tr("toolname_guiconverter", + en="GUI Converter", + zh_cn="GUI 转换", + zh_hk="GUI 轉換", + ) diff --git a/src/preppipe_gui_pyside6/navigatorwidget.py b/src/preppipe_gui_pyside6/navigatorwidget.py index 5643969..4b54913 100644 --- a/src/preppipe_gui_pyside6/navigatorwidget.py +++ b/src/preppipe_gui_pyside6/navigatorwidget.py @@ -12,6 +12,7 @@ from .toolwidgets.imagepack import * from .toolwidgets.setting import * from .toolwidgets.maininput import * +from .toolwidgets.fguiconverter import * class ToolNode: info : ToolWidgetInfo | None @@ -24,6 +25,7 @@ class ToolNode: NAVIGATION_LIST : typing.ClassVar[list] = [ SettingWidget, MainInputWidget, + FguiConverterWidget, (ImagePackWidget, {"category_kind": ImagePackDescriptor.ImagePackType.BACKGROUND}), (ImagePackWidget, {"category_kind": ImagePackDescriptor.ImagePackType.CHARACTER}), ] diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py new file mode 100644 index 0000000..2d91e18 --- /dev/null +++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py @@ -0,0 +1,244 @@ +from PySide6.QtCore import * +from PySide6.QtGui import * +from PySide6.QtWidgets import * +from preppipe.language import * +from ..forms.generated.ui_fguiconverterwidget import Ui_FguiConverterWidget +from ..toolwidgetinterface import * +from ..mainwindowinterface import * +from ..settingsdict import * +from ..translatablewidgetinterface import * +from ..util.fileopen import FileOpenHelper + +TR_gui_fguiconverter = TranslationDomain("gui_fguiconverter") + +class FguiConverterWidget(QWidget, ToolWidgetInterface): + listChanged = Signal() + isDirectoryMode : bool + isExistingOnly : bool + verifyCB : typing.Callable[[str], bool] | None + fieldName : Translatable | str + filter : Translatable | str + lastAddedPath : str + lastOutputPath : str + ui : Ui_FguiConverterWidget + + + TR_gui_fguiassetsdictwidget = TranslationDomain("gui_fguiassetsdictwidget") + _tr_fgui_assets_dict_label = TR_gui_fguiassetsdictwidget.tr("fgui_assets_dict_label", + en="FairyGUI Assets Dictionary", + zh_cn="FairyGUI资源目录", + zh_hk="FairyGUI資源目錄", + ) + _tr_add = TR_gui_fguiassetsdictwidget.tr("add", + en="Add", + zh_cn="添加", + zh_hk="添加", + ) + _tr_remove = TR_gui_fguiassetsdictwidget.tr("remove", + en="Remove", + zh_cn="删除", + zh_hk="刪除", + ) + _tr_open_directory = TR_gui_fguiassetsdictwidget.tr("open_directory", + en="Open Directory", + zh_cn="打开目录", + zh_hk="打開目錄", + ) + _tr_output_dict_label = TR_gui_fguiassetsdictwidget.tr("output_dict_label", + en="Ren'Py Project Base Directory", + zh_cn="Ren'Py项目基目录", + zh_hk="Ren'Py項目基目錄", + ) + _tr_output_dict_button = TR_gui_fguiassetsdictwidget.tr("output_dict_button", + en="Select", + zh_cn="选择", + zh_hk="選擇", + ) + _tr_output_dict_line = TR_gui_fguiassetsdictwidget.tr("output_dict_line_edit", + en="(None)", + zh_cn="(未指定)", + zh_hk="(未指定)", + ) + _tr_convert_button = TR_gui_fguiassetsdictwidget.tr("convert_button", + en="Convert", + zh_cn="转换", + zh_hk="轉換", + ) + + def __init__(self, parent : QWidget): + super(FguiConverterWidget, self).__init__(parent) + self.ui = Ui_FguiConverterWidget() + self.ui.setupUi(self) + self.isDirectoryMode = True + self.isExistingOnly = False + self.verifyCB = None + self.fieldName = "" + self.filter = "" + self.lastAddedPath = "" + self.lastOutputPath = "" + + self.bind_text(self.ui.fguiAssetDictLabel.setText, self._tr_fgui_assets_dict_label) + self.bind_text(self.ui.addDictButton.setText, self._tr_add) + self.bind_text(self.ui.removeDictButton.setText, self._tr_remove) + self.bind_text(self.ui.openDictButton.setText, self._tr_open_directory) + self.bind_text(self.ui.outputDictLabel.setText, self._tr_output_dict_label) + self.bind_text(self.ui.outputDictButton.setText, self._tr_output_dict_button) + self.bind_text(self.ui.outputDictLine.setText, self._tr_output_dict_line) + self.bind_text(self.ui.convertButton.setText, self._tr_convert_button) + + self.ui.listWidget.itemChanged.connect(lambda: self.listChanged.emit()) + self.ui.addDictButton.clicked.connect(self.itemAdd) + self.ui.removeDictButton.clicked.connect(self.itemRemove) + self.ui.openDictButton.clicked.connect(self.itemOpenContainingDirectory) + self.setAcceptDrops(True) + self.ui.outputDictButton.clicked.connect(self.itemOpenOutputDirectory) + self.ui.outputDictLine.textChanged.connect(self.setOutputDict) + + @classmethod + def getToolInfo(cls, **kwargs) -> ToolWidgetInfo: + return ToolWidgetInfo( + idstr="fguiconverter", + name="UI资源转换", + widget=cls, + ) + + + def setDirectoryMode(self, v: bool): + self.isDirectoryMode = v + if self.verifyCB is None: + self.verifyCB = (lambda path: os.path.isdir(path)) if v else (lambda path: os.path.isfile(path)) + + def setVerifyCallBack(self, cb): + """Set a callback function that verifies a path. The function should accept a string and return a boolean.""" + self.verifyCB = cb + + def setExistingOnly(self, v: bool): + self.isExistingOnly = v + + def setFieldName(self, name: Translatable | str): + self.fieldName = name + if isinstance(name, Translatable): + self.bind_text(self.ui.label.setText, name) + else: + self.ui.label.setText(name) + + def setFilter(self, filter_str: Translatable | str): + self.filter = filter_str + + def getCurrentList(self): + results = [] + for i in range(self.ui.listWidget.count()): + item = self.ui.listWidget.item(i) + results.append(item.text()) + return results + + def dragEnterEvent(self, e: QDragEnterEvent): + if e.mimeData().hasUrls(): + path = e.mimeData().urls()[0].toLocalFile() + if not self.verifyCB or self.verifyCB(path): + e.acceptProposedAction() + return + super().dragEnterEvent(e) + + def dropEvent(self, event: QDropEvent): + for url in event.mimeData().urls(): + path = url.toLocalFile() + if not self.verifyCB or self.verifyCB(path): + self.addPath(path) + event.acceptProposedAction() + + @Slot(str) + def addPath(self, path: str): + # Prevent duplicates + for i in range(self.ui.listWidget.count()): + if self.ui.listWidget.item(i).text() == path: + return + newItem = QListWidgetItem(path) + newItem.setToolTip(path) + self.ui.listWidget.addItem(newItem) + self.lastAddedPath = path + self.listChanged.emit() + + @Slot() + def itemMoveUp(self): + curRow = self.ui.listWidget.currentRow() + if curRow > 0: + item = self.ui.listWidget.takeItem(curRow) + self.ui.listWidget.insertItem(curRow - 1, item) + self.ui.listWidget.setCurrentRow(curRow - 1) + self.listChanged.emit() + + @Slot() + def itemMoveDown(self): + curRow = self.ui.listWidget.currentRow() + if curRow >= 0 and curRow + 1 < self.ui.listWidget.count(): + item = self.ui.listWidget.takeItem(curRow) + self.ui.listWidget.insertItem(curRow + 1, item) + self.ui.listWidget.setCurrentRow(curRow + 1) + self.listChanged.emit() + + @Slot() + def itemOpenContainingDirectory(self): + curRow = self.ui.listWidget.currentRow() + if curRow >= 0: + path = self.ui.listWidget.item(curRow).text() + FileOpenHelper.open_containing_directory(self, path) + + _tr_select_dialog_title = TR_gui_fguiassetsdictwidget.tr("select_dialog_title", + en="Please select FairyGUI Assets Dictionary{field}", + zh_cn="请选择FairyGUI资源目录{field}", + zh_hk="請選擇FairyGUI資源目錄{field}", + ) + + @Slot() + def itemAdd(self): + dialogTitle = self._tr_select_dialog_title.format(field=str(self.fieldName)) + initialDir = self.lastAddedPath if self.lastAddedPath else "" + dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter)) + if self.isDirectoryMode: + dialog.setFileMode(QFileDialog.Directory) + dialog.setOption(QFileDialog.ShowDirsOnly, True) + else: + dialog.setFileMode(QFileDialog.ExistingFile if self.isExistingOnly else QFileDialog.AnyFile) + dialog.fileSelected.connect(self.addPath) + dialog.show() + + @Slot() + def itemRemove(self): + curRow = self.ui.listWidget.currentRow() + if curRow >= 0: + item = self.ui.listWidget.takeItem(curRow) + del item + self.listChanged.emit() + + _tr_select_dialog_title_2 = TR_gui_fguiassetsdictwidget.tr("select_dialog_title_2", + en="Please select Ren'Py Project Base Directory{field}", + zh_cn="请选择Ren'Py项目基目录{field}", + zh_hk="請選擇Ren'Py項目基目錄{field}", + ) + + @Slot() + def itemOpenOutputDirectory(self): + dialogTitle = self._tr_select_dialog_title_2.format(field=str(self.fieldName)) + initialDir = self.lastOutputPath if self.lastOutputPath else "" + dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter)) + if self.isDirectoryMode: + dialog.setFileMode(QFileDialog.Directory) + dialog.setOption(QFileDialog.ShowDirsOnly, True) + else: + dialog.setFileMode(QFileDialog.ExistingFile if self.isExistingOnly else QFileDialog.AnyFile) + dialog.fileSelected.connect(self.setOutputDict) + dialog.show() + + @Slot(str) + def setOutputDict(self, path: str): + self.ui.outputDictLine.setText(path) + self.lastOutputPath = path + + @classmethod + def getToolInfo(cls, **kwargs) -> ToolWidgetInfo: + return ToolWidgetInfo( + idstr="guiconverter", + name=MainWindowInterface.tr_toolname_guiconverter, + widget=cls, + ) From c386335bca7bad5e8fa704e391a8bb305ce55880 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Mon, 29 Sep 2025 23:10:33 +0800 Subject: [PATCH 02/19] Add fgui_converter modul. --- src/fgui_converter/FguiAssetsParseLib.py | 1017 +++++++++++++++++ src/fgui_converter/__init__.py | 4 + .../utils/renpy/Fgui2RenpyConverter.py | 999 ++++++++++++++++ .../renpy/renpy_templates/01_renpy_cdd.rpy | 175 +++ .../renpy/renpy_templates/02_renpy_shader.rpy | 99 ++ .../renpy_ellipseAA_template.txt | 2 + .../renpy_ellipse_template.txt | 2 + .../renpy_font_map_definition.txt | 14 + .../renpy_rectangleAA_template.txt | 2 + .../renpy_rectangle_template.txt | 2 + .../toolwidgets/fguiconverter.py | 38 +- 11 files changed, 2342 insertions(+), 12 deletions(-) create mode 100644 src/fgui_converter/FguiAssetsParseLib.py create mode 100644 src/fgui_converter/__init__.py create mode 100644 src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py new file mode 100644 index 0000000..a5af312 --- /dev/null +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -0,0 +1,1017 @@ +# -*- coding: utf-8 -*- + +import sys +import os +from lxml import etree + +class FguiPackage(): + """ + FairyGUI资源包中的Package描述内容。 + 包含component列表、image列表和atlas列表。 + """ + desc_id = '' + name = '' + component_list = [] + image_list = [] + atlas_list = [] + + class brief_component: + id = '' + name = '' + path = '' + size = () + exported = True + def __init__(self, id, name, path, size, exported=True): + self.id = id + self.name = name + self.path = path + size_list = size.split(",") + self.size = (int(size_list[0]), int(size_list[1])) + self.exported = exported + + class brief_image: + id = '' + name = '' + path = '' + size = () + scale = '' # 九宫格:9grid;平铺:tile + scale9grid = [] + exported = True + def __init__(self, id, name, path, size, scale=None, scale9grid=[], exported=True): + self.id = id + self.name = name + self.path = path + size_list = size.split(",") + self.size = (int(size_list[0]), int(size_list[1])) + self.scale = scale + if self.scale == '9grid': + self.scale9grid = scale9grid.split(",") + self.exported = exported + + class brief_atlas: + id = '' + size = () + file = '' + def __init__(self, id, size, file): + self.id = id + size_list = size.split(",") + self.size = (int(size_list[0]), int(size_list[1])) + self.file = file + + def __init__(self, package_etree): + self.package_etree = package_etree + self.id = package_etree.get("id") + self.name = package_etree.get("name") + self.resource = package_etree[0] + self.id_name_mapping = {} + if (self.resource.tag != 'resources'): + raise ValueError('packageDescription child is not resources.') + for child in self.resource: + # component类 id, name, path, size, exported=True + if (child.tag == 'component'): + self.component_list.append(self.brief_component( + child.get('id'), + child.get('name'), + child.get('path'), + child.get('size'), + exported=TransStrToBoolean(child.get('exported')))) + self.id_name_mapping[child.get('id')] = child.get('name') + # image类 name, path, size, scale=None, scale9grid=[], exported=True + if (child.tag == 'image'): + self.image_list.append(self.brief_image( + child.get('id'), + child.get('name'), + child.get('path'), + child.get('size'), + child.get('scale'), + child.get('scale9grid'), + exported=TransStrToBoolean(child.get('exported')))) + self.id_name_mapping[child.get('id')] = child.get('name') + # atlas类 id, size, file + if (child.tag == 'atlas'): + self.atlas_list.append(self.brief_atlas( + child.get('id'), + child.get('size'), + child.get('file'))) + + def clear(self): + self.component_list.clear() + self.image_list.clear() + self.atlas_list.clear() + self.id_name_mapping.clear() + +def TransStrToBoolean(str): + if (str == 'true' or str == 'True'): + return True + else: + return False + +class FguiSpriteInfo: + """ + FairyGUI资源包中的Sprite描述内容。 + 总计11个字段,包括image id、图集编号、在图集中的x坐标、在图集中的y坐标、image宽度、image高度、rotate、 + 原图相对image的x偏移、原图相对image的y偏移、原图宽度、原图高度。 + """ + def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x, offset_y, source_width, source_height): + self.image_id = image_id + self.atlas_index = int(atlas_index) + self.x = int(x) + self.y = int(y) + self.width = int(width) + self.height = int(height) + self.rotate = int(rotate) + self.offset_x = int(offset_x) + self.offset_y = int(offset_y) + self.source_width = int(source_width) + self.source_height = int(source_height) + + def __repr__(self): + return f"FguiSpriteInfo({self.image_id}, {self.atlas_index}, {self.x}, {self.y}, {self.width}, {self.height}, {self.rotate}, {self.offset_x}, {self.offset_y}, {self.source_width}, {self.source_height})" + +# 解析纹理集描述文件。文件名通常为“项目名称@sprites.bytes” +def ParseFguiSpriteDescFile(sprite_desc_file): + fgui_image_sets = [] + with open(sprite_desc_file, "r", encoding="utf-8") as f: + for line in f: + parts = line.strip().split() + if len(parts) == 11: # 确保每行有11个字段 + obj = FguiSpriteInfo(*parts) + fgui_image_sets.append(obj) + return fgui_image_sets + +# 从xml字符串创建lxml的etree对象 +def GetXmlTree(xml_str): + root = etree.fromstring(xml_str.encode('utf-8')) + return root + +# 解析发布资源描述文件。文件名通常为“项目名称.bytes” +def ParseFguiPackageDescFile(file_name): + #f = open("Package1.bytes", "r", encoding='utf-8') + with open(file_name, "r", encoding='utf-8') as f: + ori_str = f.read() + xml_flag_str = '.xml' + split_flag_str = '|' + xml_length = 0 + cursor = 0 + index = 0 + xml_string = '' + xml_name = '' + object_dict = {} + index = ori_str.find(xml_flag_str) + + while index != -1: + xml_name = ori_str[cursor:index] + cursor= index+len(xml_flag_str) + index = ori_str.find(split_flag_str, cursor, -1) + cursor = index + len(split_flag_str) + index = ori_str.find(split_flag_str, cursor, -1) + xml_length = int(ori_str[cursor:index]) + cursor = index + len(split_flag_str) + index = cursor + xml_length + xml_string = ori_str[cursor: index] + object_dict[xml_name] = GetXmlTree(xml_string) + cursor = index + index = ori_str.find(xml_flag_str, cursor) + + return object_dict + + +class OriImage: + """ + 图像类。 + 结合两个描述文件信息的原始image类。 + 为后续生成目标引擎图像定义的基础数据结构。 + svg生成的图片可能会带有@2x、@3x等后缀,导致图片尺寸有异,此处暂时忽略。 + """ + def __init__(self, image_id, name, atlas_name, atlas_pos_x, atlas_pos_y, width, height): + self.image_id = image_id + self.name = name + self.atlas_name = atlas_name + self.atlas_pos_x = atlas_pos_x + self.atlas_pos_y = atlas_pos_y + self.width = width + self.height = height + + def __repr__(self): + return f"OriImage({self.image_id}, {self.name}, {self.atlas_name}, {self.atlas_pos_x}, {self.atlas_pos_y}, {self.width}, {self.height})" + + +def GetOriImage(image_id, package_desc, fgui_image_sets, fgui_atlas_dicts): + image_set = None + for item in fgui_image_sets: + if (item.image_id == image_id): + image_set = item + atlas_name = fgui_atlas_dicts["atlas"+str(item.atlas_index)] + for image in package_desc.image_list: + if (image.id == image_id): + name =image.name + return OriImage(image_id, name, atlas_name, image_set.x, image_set.y, image_set.width, image_set.height) + + +class FguiController: + """ + FairyGUI控制器类 + name:控制器名称,字符串,样例"button" + page:索引、索引名,字符串,样例"0,up,1,down,2,over,3,selectedOver" + selected:初始索引号,字符串 ,样例"0" + """ + def __init__(self, name, page, selected): + self.name = name + self.page_index_dict = {} + page_list = page.split(',') + page_num = len(page_list) + i = 0 + while i < page_num: + self.page_index_dict[int(page_list[i])] = page_list[i+1] + i += 2 + self.selected = int(selected) if selected else 0 + + def __repr__(self): + return f"FguiController({self.name}, {self.page_index_dict}, {self.selected})" + +class FguiHitTest: + """ + 点击测试区域类。 + """ + def __init__(self, hit_test_str): + self.src = None + self.pos = (0, 0) + if hit_test_str: + src, xpos, ypos = hit_test_str.split(",") + self.src = src + self.pos = (int(xpos), int(ypos)) + +class FguiComponent: + """ + FairyGUI组件对应的基类。 + id、name、path和size属性与package中brief_component的对应组件信息保持一致 + """ + + def __init__(self, component_etree, id, name, package_desc_id=None): + self.component_etree = component_etree + self.id = id + self.name = name + self.package_desc_id = package_desc_id + self.size = component_etree.get("size") + self.extention = component_etree.get("extention") + self.mask = component_etree.get("mask") + # 轴心。默认值为(0.0, 0.0)。 + pivot = component_etree.get("pivot", "0.0,0.0") + self.pivot = tuple(map(float, pivot.split(","))) + # 点击测试区域,包含3个字段,分别为引用源src和组件内相对坐标x、y。 + hit_test = component_etree.get("hitTest") + self.hit_test = FguiHitTest(hit_test) if hit_test else None + + # 控制器,一般不超过1个。 + self.controller_list = [] + # 显示内容列表,通常为image、text、graph。 + self.display_list = None + # 一级子对象 + self.child_num = len(self.component_etree) + + for i in range(self.child_num): + # 控制器 + if (self.component_etree[i].tag == "controller"): + self.controller_list.append(FguiController(self.component_etree[i].get("name"), self.component_etree[i].get("pages"), self.component_etree[i].get("selected"))) + # 显示内容 + elif (self.component_etree[i].tag == "displayList"): + self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id) + + def __repr__(self): + return f"FguiComponent({self.id}, {self.name}, {self.size}, {self.extention}, {self.mask})" + +class FguiButton(FguiComponent): + """ + FairyGUI中的按钮button。 + 相比其他component,多一个Button标签 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + button = component_etree.find("Button") + self.button_mode = button.get("mode") + self.button_down_effect = button.get("downEffect") + self.button_down_effect_value = button.get("downEffectValue") + +class FguiScrollBar(FguiComponent): + """ + FairyGUI中的滚动条scrollbar。 + 相比其他component,多一个ScrollBar标签,大部分情况为空。 + 通常滚动条都会有以个对应的其他组件,为同名带后缀“_grip”的按钮。 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + scrollbar = component_etree.find("ScrollBar") + +class FguiLabel(FguiComponent): + """ + FairyGUI中的标签lablel。 + 目前与FguiComponent完全相同,甚至不具有单独的Label标签。 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + + +class FguiComboBox(FguiComponent): + """ + FairyGUI中的下拉框。 + 相比其他component,多一个ComboBox标签,属性dropdown对应点击后显示的选项列表。 + 通常下拉框都会有两个对应的其他组件,分别为同名带后缀“_item”的按钮和同名带后缀“_popup”的组件 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + combobox = component_etree.find("ComboBox") + self.dropdown = combobox.get("dropdown") + +class FguiProgressBar(FguiComponent): + """ + FairyGUI中的进度条。 + 相比其他component,多一个ProgressBar标签,大部分情况为空。 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + combobox = component_etree.find("ProgressBar") + +class FguiSlider(FguiComponent): + """ + FairyGUI中的滑动条。 + 相比其他component,多一个Slider标签,大部分情况为空。 + 滑动条会有一个相应的其他组件,同名带后缀“_grip”的按钮。 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + combobox = component_etree.find("Slider") + +class FguiWindow(FguiComponent): + """ + FairyGUI中的可拖拽窗口。 + 没有extention类型,也没有特殊标签,仅从扩展类型无法区分Label和Window。 + + FairyGUI中的Window子组件有一些非硬性命名约定(约束): + 1. 名称为frame的组件,作为Window的背景。该子组件通常扩展类型为“Label”。 + 2. frame的组件内部,一个名称为closeButton的按钮将自动作为窗口的关闭按钮。 + 3. frame的组件内部,一个名称为dragArea的图形(类型设置为空白)将自动作为窗口的检测拖动区域。 + 4. frame的组件内部,一个名称为contentArea的图形(类型设置为空白)将作为窗口的主要内容区域。 + 根据以上约定制作的Window,由各引擎内部的FairyGUI的相关包负责动态创建、渲染和事件响应。 + + Ren'Py中没有FairyGUI的包体处理以上内容,且通常不需要动态创建组件。 + 暂定按默认Component处理。可拖拽功能再议。 + """ + def __init__(self, component_etree, id, name, package_desc_id=None): + super().__init__(component_etree, id, name, package_desc_id=None) + + +class FguiDisplayList: + """ + FairyGUI组件内部显示列表,xml中displayList标签内容。 + """ + def __init__(self, display_list_etree, package_desc_id=None): + self.display_list_etree = display_list_etree + self.package_desc_id = package_desc_id + # print(f"package_desc_id: {package_desc_id}") + self.displayable_list = [] + for displayable in self.display_list_etree: + print(displayable.tag) + # 根据标签类型创建相应的FguiDisplayable对象 + if displayable.tag == "graph": + self.displayable_list.append(FguiGraph(displayable)) + elif displayable.tag == "text": + self.displayable_list.append(FguiText(displayable)) + elif displayable.tag == "image": + self.displayable_list.append(FguiImage(displayable)) + elif displayable.tag == "list": + self.displayable_list.append(FguiList(displayable, self.package_desc_id)) + elif displayable.tag == "loader": + self.displayable_list.append(FguiLoader(displayable)) + else: + # 对于未知类型,创建基础的FguiDisplayable对象 + self.displayable_list.append(FguiDisplayable(displayable)) + +def hex_aarrggbb_to_rgba(hex_color): + """ + 将一个 8 位的十六进制颜色字符串 (AARRGGBB) 转换为一个 RGBA 元组。 + """ + # 移除字符串头部的 '#' + clean_hex = hex_color.lstrip('#') + + # 检查处理后的字符串长度是否为 8 + if len(clean_hex) != 8: + raise ValueError("输入的十六进制字符串必须是8位 (AARRGGBB)") + + # 按 AARRGGBB 的顺序提取,并从16进制转换为10进制整数 + try: + a = int(clean_hex[0:2], 16) + r = int(clean_hex[2:4], 16) + g = int(clean_hex[4:6], 16) + b = int(clean_hex[6:8], 16) + except ValueError: + raise ValueError("字符串包含无效的十六进制字符") + + return (r, g, b, a) + +def rgba_normalize(rgba_tuple): + r = float(rgba_tuple[0]/255) + g = float(rgba_tuple[1]/255) + b = float(rgba_tuple[2]/255) + a = float(rgba_tuple[3]/255) + return (r, g, b, a) + +class ColorFilterData: + """ + 颜色滤镜数据,总共4项,分别为亮度、对比度、饱和度、色相。 + """ + def __init__(self, data_string): + if not data_string: + raise ValueError("Color Filter Data is Null.") + self.brightness, self.contrast, self.saturation, self.hue = map(float, data_string.split(",")) + + +class FguiDisplayable: + """ + FairyGUI组件内显示对象。 + 可能的类型包括:graph(图形)、image(图片)、text(文字)、component(组件)、list(列表)、loader(装载器)。 + 基本属性:id、名称、引用源、位置、尺寸、缩放、倾斜、轴心、锚点、不透明度、旋转、是否可见、是否变灰、是否可触摸。 + + 此外,fgui中的“效果”与“其他”配置也放在属性中。 + + 支持多种子项。其中Button项仅限按钮作为子组件时才可能存在。 + relation(暂未处理)和FGUI自身gearBase的派生类。 + 在一个FguiDisplayable对象中,以下各类gear均至多只存在一个。 + 除了 *是否显示* 可以由至多两个控制器决定,其他属性均只能由单一控制器决定。 + gearBase的派生类如下: + gearDisplay-根据控制器决定是否显示; + gearDisplay2-协同另一个控制器决定是否显示; + gearLook-根据控制器决定外观transform,包括alpha、rotation、grayed、touchable等; + gearXY-根据控制器决定坐标; + gearSize-根据控制器决定尺寸,包括size和scale; + gearColor-根据控制器决定“图片-颜色”; + gearText-根据控制器决定文本组件显示的文本内容; + gearIcon-根据控制器决定装载器显示内容。 + """ + def __init__(self, display_item_tree, package_description_id=None): + self.display_item_tree = display_item_tree + # id + self.id = self.display_item_tree.get("id") + # 名称 + self.name = self.display_item_tree.get("name") + # 引用源,通常是id。若为图片可在FguiAssets的package_desc.image_list中根据id查找图片名。若为组件可在FguiAssets的package_desc.component_list中根据id查找组件。 + self.src = self.display_item_tree.get("src") + # 位置 + xy = self.display_item_tree.get("xy", "0,0") + self.xypos = tuple(map(int, xy.split(","))) + # 尺寸。若为None:image的size默认与image对象一致。 + size = self.display_item_tree.get("size") + self.size = tuple(map(int, size.split(","))) if size else None + # 保持比例。若为None,则将宽和高分别缩放到size。 + self.aspect = (self.display_item_tree.get("aspect") == "true") + # 缩放。默认值为(1.0, 1.0)。 + scale = self.display_item_tree.get("scale", "1.0,1.0") + self.scale = tuple(map(float, scale.split(","))) + # 倾斜。默认值为(0, 0)。 + skew = self.display_item_tree.get("skew", "0,0") + self.skew = tuple(map(int, skew.split(","))) + # 轴心。默认值为(0.0, 0.0)。 + pivot = self.display_item_tree.get("pivot", "0.0,0.0") + self.pivot = tuple(map(float, pivot.split(","))) + # 是否将轴心作为锚点。否认为False。 + self.pivot_is_anchor = (self.display_item_tree.get("anchor") == "true") + # 不透明度。默认为1.0。 + alpha = self.display_item_tree.get("alpha", "1.0") + self.alpha = float(alpha) + # 旋转。默认值为0。 + rotation = self.display_item_tree.get("rotation", "0") + self.rotation = int(rotation) + # 未明确为不可见则默认可见 + self.visible = not (self.display_item_tree.get("visible") == "false") + # 未明确是否变灰则默认不变灰 + self.grayed = (self.display_item_tree.get("grayed") == "true") + # 未明确是否可触摸则默认可以触摸 + self.touchable = not (self.display_item_tree.get("touchable") == "false") + # 资源包描述id,部分组件需要 + self.package_description_id = package_description_id + # BlendMode + self.blend_mode = self.display_item_tree.get("blend", "normal") + # 滤镜 + self.color_filter = self.display_item_tree.get("filter") + # 滤镜颜色变换 + self.color_filter_values = ColorFilterData(self.display_item_tree.get("filterData", "1,1,1,1")) + # Tooltips,一般指指针悬垂在组件上时显示的说明文本。 + self.tooltips = self.display_item_tree.get("tooltips") + # 自定义数据 + self.custom_data = self.display_item_tree.get("customData") + + # Button,按钮专有属性 + self.button_property = None + + # gear属性 + self.gear_display = None + self.gear_display_2 = None + self.gear_pos = None + self.gear_look = None + self.gear_size = None + self.gear_color = None + self.gear_text = None + self.gear_icon = None + + # 一级子对象 + self.child_num = len(self.display_item_tree) + # 控制器gear子组件 + for i in range(self.child_num): + if self.display_item_tree[i].tag == "gearDisplay" : + self.gear_display = FguiGearDisplay(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearDisplay2" : + self.gear_display_2 = FguiGearDisplay(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearXY" : + self.gear_pos = FguiGearPos(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearSize" : + self.gear_size = FguiGearSize(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearLook" : + self.gear_look = FguiGearLook(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearColor" : + self.gear_color = FguiGearColor(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearText" : + self.gear_text = FguiGearText(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "gearIcon" : + self.gear_icon = FguiGearIcon(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "Button" : + self.button_property = FguiButtonProperty(self.display_item_tree[i]) + else: + print(f"Tag not parse: {self.display_item_tree[i].tag}.") + + def __repr__(self): + return f"FguiDisplayable({self.id}, {self.name}, {self.xypos}, {self.size}, \ +{self.scale}, {self.skew}, {self.pivot}, {self.pivot_is_anchor}, {self.alpha}, \ +{self.rotation}, {self.visible}, {self.grayed}, {self.touchable})" + +class FguiComponentPropertyBase: + """ + 组件子属性信息基类 + """ + def __init__(self, component_property_tree): + self.component_property_tree = component_property_tree + self.property_name = self.component_property_tree.tag + +class FguiButtonProperty(FguiComponentPropertyBase): + """ + 组件子属性中的Button信息。 + 通常包括title和icon,分别表示标题(文本)和图标(装载器) + """ + def __init__(self, component_property_tree): + super().__init__(component_property_tree) + if self.property_name != "Button" : + raise ValueError("xml tag is not Button") + self.title = self.component_property_tree.get("title") + self.selected_title = self.component_property_tree.get("selectedTitle") + self.icon = self.component_property_tree.get("icon") + self.selected_icon = self.component_property_tree.get("selectedIcon") + + +class FguiGraph(FguiDisplayable): + """ + FairyGUI中的图形。包括空白、矩形(圆边矩形)、圆形(椭圆)、多边形等。 + """ + def __init__(self, display_item_tree): + if display_item_tree.tag != "graph" : + raise ValueError("xml tag is not graph.") + super().__init__(display_item_tree) + # None: 空白 rect: 矩形(可带圆角) eclipse: 椭圆(包括圆形) regular_polygon: 正多边形 polygon: 多边形 + self.type = self.display_item_tree.get("type", None) + self.stroke_width = int(self.display_item_tree.get("lineSize", "1")) + stroke_color= self.display_item_tree.get("lineColor", "#ff000000") # 描边默认为黑色 + self.stroke_color = hex_aarrggbb_to_rgba(stroke_color) + fill_color = self.display_item_tree.get("fillColor", "#ffffffff") # 描边默认为白色 + self.fill_color = hex_aarrggbb_to_rgba(fill_color) + # 矩形可能存在圆角 + self.corner_radius = int(self.display_item_tree.get("corner", "0")) + # 正多边形需要记录边数和顶点位置。 + # 顶点位置使用一个数组表示,数组长度等于顶点数(边数)。 + # 顶点只能存在于标准正多边形顶点到图形中心的连线上。 + # 每个数字表示对应顶点到图形中心的距离,最大值为1.0,最小值为0.0。 + if (self.type == "regular_polygon"): + self.sides = int(self.display_item_tree.get("sides", "3")) + distances = self.display_item_tree.get("distances", "1.0,"*(self.sides-1)+"1.0") + distances_list = distances.split(",") + for i in range(len(distances_list)): + if distances_list[i] == '': + distances_list[i] = "1.0" + # 多边形只记录顶点坐标。 + # points为顺序连接的一组xy坐标,例如"5,4,12,-2,20.78191,5.746792,14,16,3,14"。 + # 虽然发布的资源xml中坐标是浮点,但FairyGUI编辑器却显示为整型。可考虑转换时也缩小精度为整型。 + if (self.type == "polygon"): + point_list = self.display_item_tree.get("points", "0.0,0.0").split(",") + iteration = iter(point_list) + self.points = tuple((float(x), float(y)) for x, y in zip(iteration, iteration)) + +class FguiText(FguiDisplayable): + """ + FairyGUI中的文本。 + """ + def __init__(self, display_item_tree): + if display_item_tree.tag != "text" : + raise ValueError("xml tag is not text.") + super().__init__(display_item_tree) + # FairyGUI编辑器未设置全局字体的情况下,默认渲染使用Arial,与Ren'Py默认字体SourceHanSansLite不同。 + # 此处的字体与FairyGUI编辑器中的字体属性不同,为字体名称。编辑器中的xml则记录引用项。 + # 另外,FairyGUI发布的资源文件中并不包含字体文件,需要手工放置到游戏引擎对应目录。 + self.text = self.display_item_tree.get("text", "") + self.font = self.display_item_tree.get("font") + self.font_size = int(self.display_item_tree.get("fontSize", 24) ) + self.text_color = self.display_item_tree.get("color", "#000000") + self.align = self.display_item_tree.get("align", "left") # 共有left、center、right三种 + self.v_align = self.display_item_tree.get("vAlign", "top") # 共有top、middle、bottom三种 + self.ubb = (self.display_item_tree.get("ubb") == "true") # UBB语法暂不考虑 + self.auto_size = self.display_item_tree.get("autoSize") + self.letter_spacing = self.display_item_tree.get("letterSpacing", 0) # 字间距 + self.leading = self.display_item_tree.get("leading", 3) # 行间距 + self.underline = (self.display_item_tree.get("underline") == "true") + self.bold = (self.display_item_tree.get("bold") == "true") + self.italic = (self.display_item_tree.get("italic") == "true") + self.strike = (self.display_item_tree.get("strike") == "true") + self.single_line = (self.display_item_tree.get("singleLine") == "true") + self.stroke_color = self.display_item_tree.get("strokeColor") + self.stroke_size = self.display_item_tree.get("strokeSize", 1) + self.shadow_color = self.display_item_tree.get("shadowColor") + shadow_offset = self.display_item_tree.get("shadowOffset") + if shadow_offset: + self.shadow_offset = tuple(map(int, shadow_offset.split(","))) + else: + self.shadow_offset = (0, 0) + # 下面几项仅限输入框 + self.is_input = (self.display_item_tree.get("input") == "true") + self.prompt = self.display_item_tree.get("prompt") + self.max_length = int(self.display_item_tree.get("maxLength", 0)) + self.restrict = self.display_item_tree.get("restrict") #输入文本规则,正则表达式字符串 + self.is_password = (self.display_item_tree.get("password") == "true") + + + +class FguiImage(FguiDisplayable): + """ + FairyGUI中的图片。 + 自身没有什么独特属性。 + """ + def __init__(self, display_item_tree): + if display_item_tree.tag != "image" : + raise ValueError("xml tag is not image.") + super().__init__(display_item_tree) + +class FguiListItem(): + """ + 列表元素,仅会在list内部。 + tag为item。 + 属性如下: + url-引用资源url,格式为“ui://”+“packageDescription id” + “component id”。若为空表示使用列表默认元素。 + title:标题,通常用于按钮。 + icon:图标,通常用于按钮。 + name:名称,没用。 + 样例: + + """ + def __init__(self, list_item_tree, package_description_id=None): + if list_item_tree.tag != "item": + raise ValueError("xml tag is not item.") + self.list_item_tree = list_item_tree + self.package_description_id = package_description_id + self.item_url = self.list_item_tree.get("url") + self.item_title = self.list_item_tree.get("title") + self.item_icon = self.list_item_tree.get("icon") + self.item_name = self.list_item_tree.get("name") + def __repr__(self): + return f"FguiListItem({self.item_url}, {self.item_title}, {self.item_icon}, {self.item_name})" + + +class FguiList(FguiDisplayable): + """ + FairyGUI中的列表。 + 列表存在“树视图”,暂不考虑。 + 列表的属性如下: + layout-列表布局:(column)默认-单列竖排,row-单行横排,flow_hz-横向流动,flow_vt-纵向流动、pagination-分页 + overflow-溢出处理:(visible)默认-可见,hidden-隐藏,scroll-滚动 + scroll-滚动条方向:(vertical)默认-垂直滚动,horizontal-水平滚动,both-自由滚动(同时允许垂直滚动和水平滚动) + scrollBar-显示滚动条:visible-可见,hidden-隐藏,auto-滚动时显示 + scrollBarFlags:一系列滚动条标识位,可能是一个12bit整数。从低位到高位分别为: + 0-bit:垂直滚动条显示在左边 + 1-bit:滚动位置自动贴近元件 + 2-bit:仅在内容溢出时才显示滚动条 + 3-bit:页面模式 + 4-bit 5-bit:触摸滚动效果,00默认、01启用、10关闭 + 6-bit 7-bit:边缘回弹效果,00默认、01启用、10关闭 + 8-bit:禁用惯性 + 9-bit:禁用剪裁 + 10-bit:浮动显示 + 11-bit:禁用剪裁边缘 + 当列表允许滚动时,有一大堆特性暂不处理,例如“边缘回弹”、指定滚动条组件、自动贴近元件等。 + margin:边缘留空,4个整数,分别对应上下左右。 + clipSoftness:边缘虚化,xy分辨对应水平与垂直方向的虚化程度。 + lineItemCount:列表布局为横向流动或分页时,表示列数。列表布局为竖向流动时,表示行数。其他布局中,该参数无效果。 + lineItemCount2:列表布局为分页时,表示行数。其他布局中,该参数无效果。 + lineGap:行距。 + colGap:列距。 + defaultItem:默认元素,通常是一个资源url,格式为“ui://”+“packageDescription id” + “component id”。 + """ + def __init__(self, display_item_tree, package_description_id=None): + if display_item_tree.tag != "list" : + raise ValueError("xml tag is not list.") + super().__init__(display_item_tree) + self.layout = self.display_item_tree.get("layout", "column") + self.overflow = self.display_item_tree.get("overflow", "visible") + self.scroll = self.display_item_tree.get("scroll", "vertical") + self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "256")) + margin = self.display_item_tree.get("margin") + self.margin = tuple(map(int, margin.split(","))) if margin else None + self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0") + self.line_item_count = int(self.display_item_tree.get("lineItemCount", "1")) + self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "1")) + self.line_gap = int(self.display_item_tree.get("lineGap", "0")) + self.col_gap = int(self.display_item_tree.get("colGap", "0")) + self.default_item_url = self.display_item_tree.get("defaultItem") + self.default_item_id = None + self.package_description_id = package_description_id + if package_description_id: + self.get_default_item(package_description_id) + self.item_list = [] + for item_tree in display_item_tree: + item = FguiListItem(item_tree, self.package_description_id) + self.item_list.append(item) + + def get_default_item(self, packageDescription_id): + self.default_item_id = self.default_item_url[self.default_item_url.find(packageDescription_id)+len(packageDescription_id):] + + +class FguiLoader(FguiDisplayable): + """ + FairyGUI中的装载器。 + 包含属性url:表示引用的组件url,通常是一个资源url,格式为“ui://”+“packageDescription id” + “component id”。 + """ + def __init__(self, display_item_tree, package_description_id=None): + if display_item_tree.tag != "loader" : + raise ValueError("xml tag is not loader.") + super().__init__(display_item_tree) + self.url = self.display_item_tree.get("url") + self.item_url = None + if package_description_id: + self.package_description_id = package_description_id + get_item_id(package_description_id) + + def get_item_id(packageDescription_id): + self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):] + +class FguiGearBase: + """ + Displayable控制器设置相关的基类。 + 必定包含controller属性。 + 可能包含page、value、default、tween属性。 + controller: 相关控制器名。 + page: 相关控制器索引。可能存在多个索引值,使用逗号分隔。 + value:与控制器索引对应的值,具体格式和作用根据Gear类型决定。若存在多个值则使用“|”分割。 + default:默认值。page属性未列出的控制器索引使用该默认值。 + tween:是否启用缓动。 + """ + def __init__(self, gear_item_tree): + self.gear_item_tree = gear_item_tree + self.controller_name = gear_item_tree.get("controller") + self.controller_index = None + controller_index = gear_item_tree.get("pages") + values = gear_item_tree.get("values") + if controller_index: + self.controller_index = controller_index.split(",") + self.values = None + if values: + self.values = values.split("|") + self.default = gear_item_tree.get("default") + self.tween = True if (gear_item_tree.get("tween") == "true") else False + + +class FguiGearDisplay(FguiGearBase): + """ + Displayable中控制器与显示相关的设置。 + 只在指定控制器的索引等于指定索引时才会显示displayable。 + 唯一至多存在2个的gear,tag名称分别为gearDisplay、gearDisplay2。 + 可由两个控制器同时控制是否显示。 + 两个控制器的控制逻辑可以是“与”,或者“或”。 + 例: + + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearDisplay" and gear_item_tree.tag != "gearDisplay2": + raise ValueError(f"xml tag is {gear_item_tree.tag}, not gearDisplay.") + super().__init__(gear_item_tree) + # condition=0——“与”逻辑;condition=1——“或”逻辑 + condition = gear_item_tree.get("condition") + self.condition = 0 + if condition: + self.condition = int(condition) + +class FguiGearPos(FguiGearBase): + """ + Displayable中控制器与位置相关的设置。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,value是一个使用竖线 ‘|’ 连接的 ‘x,y’形式坐标列表。 + 若pages包含多个控制器索引,values则是多个固定长度2列表,使用竖线 ‘|’ 连接。 + 例: + + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearXY" : + raise ValueError("xml tag is not gearXY.") + super().__init__(gear_item_tree) + value = gear_item_tree.get("value") + self.index_value_dict = {} # 该字典存放控制器索引与坐标 + if self.values: + for i in range(len(self.values)): + xypos = tuple(map(int, self.values[i].split(","))) + self.index_value_dict[self.controller_index[i]] = xypos + if self.default: + xypos = tuple(map(int, self.default.split(","))) + self.index_value_dict["default"] = xypos + +class FguiGearLook(FguiGearBase): + """ + Displayable中控制器与外观相关的设置。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,values是一个使用逗号连接的固定长度4列表,分别对应透明度、旋转、变灰、不可触摸。 + 若pages包含多个控制器索引,values则是多个固定长度4列表,使用竖线 ‘|’ 连接。 + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearLook" : + raise ValueError("xml tag is not gearLook.") + super().__init__(gear_item_tree) + self.index_value_dict = {} # 该字典存放控制器索引与对应透明度、旋转、变灰、不可触摸 + if self.values: + for i in range(len(self.values)): + item = self.values[i].split(",") + alpha = float(item[0]) + rotation = int(item[1]) + grayed = False if (item[2] != '0') else True + touchable = True if (item[3] == '1') else False + self.index_value_dict[self.controller_index[i]] = (alpha, rotation, grayed, touchable) + if self.default: + item = self.default.split(",") + alpha = float(item[0]) + rotation = int(item[1]) + grayed = False if (item[2] != '0') else True + touchable = True if (item[3] == '1') else False + self.index_value_dict["default"] = (alpha, rotation, grayed, touchable) + +class FguiGearSize(FguiGearBase): + """ + Displayable中控制器与尺寸相关的设置。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,values是一个使用逗号连接的固定长度4列表,分别对应宽度、高度、宽度缩放系数、高度缩放系数。 + 若pages包含多个控制器索引,values则是多个固定长度4列表,使用竖线 ‘|’ 连接。 + 例: + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearSize" : + raise ValueError("xml tag is not gearSize.") + super().__init__(gear_item_tree) + self.index_value_dict = {} # 该字典存放控制器索引与对应尺寸 + if self.values: + for i in range(len(self.values)): + item = index_values[i].split(",") + width = int(item[0]) + height = int(item[1]) + xscale = float(item[2]) + yscale = float(item[3]) + self.index_value_dict[self.controller_index[i]] = (width, height, xscale, yscale) + if self.default: + item = self.default.split(",") + width = int(item[0]) + height = int(item[1]) + xscale = float(item[2]) + yscale = float(item[3]) + self.index_value_dict["default"] = item + +class FguiGearColor(FguiGearBase): + """ + Displayable中控制器与一个与图像颜色相乘的颜色设置。默认为“#ffffff”,白色,相乘后无视觉变化。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,values是一个24位的十六进制颜色值。 + 若pages包含多个控制器索引,values则是多个十六进制颜色值,使用竖线 ‘|’ 连接。 + 例: + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearColor" : + raise ValueError("xml tag is not gearColor.") + super().__init__(gear_item_tree) + self.index_value_dict = {} # 该字典存放控制器索引与对应颜色 + if self.values: + for i in range(len(self.values)): + self.index_value_dict[self.controller_index[i]] = self.values[i] + if self.default: + self.index_value_dict["default"] = self.default + +class FguiGearText(FguiGearBase): + """ + 只会出现在文本组件中才生效的控制属性。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,values是一个字符串。 + 若pages包含多个控制器索引,values则是字符串,使用竖线 ‘|’ 连接。 + 例: + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearText" : + raise ValueError("xml tag is not gearText.") + super().__init__(gear_item_tree) + self.index_value_dict = {} # 该字典存放控制器索引与对应文本 + if self.values: + for i in range(len(self.values)): + self.index_value_dict[self.controller_index[i]] = self.values[i] + if self.default: + self.index_value_dict["default"] = self.default + +class FguiGearIcon(FguiGearBase): + """ + 只会出现在装载器组件中才生效的控制属性。 + 属性values的值与属性pages有关。 + 若pages只有一个控制值索引,values是一个资源url,格式为“ui://”+“packageDescription id” + “component id”。 + 若pages包含多个控制器索引,values是多个资源url,使用竖线 ‘|’ 连接。 + 例: + + """ + def __init__(self, gear_item_tree): + if gear_item_tree.tag != "gearIcon" : + raise ValueError("xml tag is not gearIcon.") + super().__init__(gear_item_tree) + self.index_value_dict = {} # 该字典存放控制器索引与显示内容 + if self.values: + for i in range(len(self.values)): + self.index_value_dict[self.controller_index[i]] = self.values[i] + if self.default: + self.index_value_dict["default"] = self.default + +class FguiAssets(): + """ + 资源解析入口。 + """ + def __init__(self, fgui_project_path): + if not fgui_project_path: + raise ValueError("Project path is illegal.") + self.fgui_project_name = os.path.basename(fgui_project_path) + self.package_desc_file = os.path.join(fgui_project_path, f"{self.fgui_project_name}.bytes") + # 发布的描述文件 + self.package_desc = None + self.object_dict = ParseFguiPackageDescFile(self.package_desc_file) + # 图集和图像描述文件 + self.sprite_desc_file = os.path.join(fgui_project_path, f"{self.fgui_project_name}@sprites.bytes") + self.fgui_image_set = [] + self.fgui_atlas_dicts = {} + # 组件信息 + self.fgui_component_set = [] + + # 先找到packageDescription,解析出component、image和atlas列表 + package_key = 'package' + if (not self.object_dict.__contains__(package_key)): + raise ValueError('Could not find package description.') + package_value = self.object_dict.get(package_key) + if self.package_desc: + self.package_desc.clear() + self.package_desc = FguiPackage(package_value) + print("This package includes", len(self.package_desc.component_list), "component(s).") + for component in self.package_desc.component_list: + if (not self.object_dict.__contains__(component.id)): + raise ValueError('Could not find component info.') + extention_type = self.object_dict[component.id].get("extention") + # 根据extention构造不同对象 + if extention_type == "Button": + component = FguiButton(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id) + elif extention_type == "ScrollBar": + component = FguiScrollBar(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id) + elif extention_type == "Label": + component = FguiLabel(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id) + elif extention_type == "Slider": + component = FguiSlider(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id) + else: + component = FguiComponent(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id) + self.fgui_component_set.append(component) + # 根据atlas_list建立altas_id与实际图集文件间的映射关系 + for atlas in self.package_desc.atlas_list: + atlas_file_name = self.fgui_project_name + '@' + atlas.file + self.fgui_atlas_dicts[atlas.id] = atlas_file_name + # 若不需要从atlas切割出单个图片,可以结合 *@sprites.bytes文件,获取每个image对象 + self.fgui_image_set = ParseFguiSpriteDescFile(self.sprite_desc_file) + + def clear(self): + self.fgui_project_name = '' + self.package_desc.clear() + self.object_dict.clear() + self.fgui_atlas_dicts.clear() + self.fgui_component_set.clear() + + def get_componentname_by_id(self, id): + return self.package_desc.id_name_mapping[id] + + def get_component_by_id(self, id): + for component in self.fgui_component_set: + if component.id == id: + return component + return None + + def get_image_size_by_id(self, id): + for image in self.fgui_image_set: + if image.image_id == id: + return (image.width, image.height) diff --git a/src/fgui_converter/__init__.py b/src/fgui_converter/__init__.py new file mode 100644 index 0000000..385d8b7 --- /dev/null +++ b/src/fgui_converter/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +# 将该目录视为可导入的 Python 包。 + diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py new file mode 100644 index 0000000..0508eff --- /dev/null +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -0,0 +1,999 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os +import re +import argparse + +import shutil + +# 添加当前目录到Python路径,以便导入FguiAssetsParseLib +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from fgui_converter.FguiAssetsParseLib import * + +class FguiToRenpyConverter: + """ + FairyGUI到Ren'Py转换器 + 将FairyGUI资源转换为Ren'Py的screen语言 + """ + + def __init__(self, fgui_assets): + self.fgui_assets = fgui_assets + self.renpy_code = [] + self.screen_code = [] + self.screen_definition_head = [] + self.screen_variable_code = [] + self.screen_function_code = [] + self.screen_ui_code = [] + self.style_code = [] + + # dismiss用于取消一些组件的focus状态,例如input。 + self.screen_has_dismiss = False + self.dismiss_action_list = [] + + # 文本对齐的转义字典 + self.align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0} + + # 字体名称列表。SourceHanSansLite为Ren'Py默认字体。 + self.font_name_list = ["SourceHanSansLite"] + + # 4个空格作为缩进基本单位 + self.indent_unit = ' ' + # 组件缩进级别 + self.root_indent_level = 0 + # 缩进字符串 + self.indent_str = '' + + # 默认背景色,在某些未指定image的情况下用作填充。 + self.default_background = '#fff' + + # 部分模板与预设目录 + # self.renpy_template_dir = 'renpy_templates' + self.renpy_template_dir = os.environ.get('RENPY_TEMPLATES_DIR', + os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates")) + self.font_map_template = 'renpy_font_map_definition.txt' + self.graph_template_dict = {} + self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt') + self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt') + + def calculate_indent(self): + self.indent_str = self.indent_unit * self.root_indent_level + return self.indent_str + + def indent_level_up(self, levelup=1): + self.root_indent_level += levelup + self.indent_str = self.indent_unit * self.root_indent_level + + def indent_level_down(self, leveldown=1): + self.root_indent_level = max(self.root_indent_level-leveldown, 0) + self.indent_str = self.indent_unit * self.root_indent_level + + def reset_indent_level(self, indent_level=0): + self.root_indent_level = indent_level + self.indent_str = '' + + def generate_image_definitions(self): + """生成图像定义""" + image_definitions = [] + image_definitions.append("# 图像定义") + image_definitions.append("# 从FairyGUI图集中提取的图像") + + for sprite in self.fgui_assets.fgui_image_set: + # 找到对应的图像信息 + image_info = None + image_name = '' + image_scale = None + image_scale9grid = None + for img in self.fgui_assets.package_desc.image_list: + if img.id == sprite.image_id: + image_info = sprite + image_name = img.name + image_scale = img.scale + image_scale9grid = img.scale9grid + break + + # GetOriImage(image_id, package_desc, fgui_image_sets, fgui_atlas_dicts) + + if image_info: + atlas_index = image_info.atlas_index + atlas_key = f"atlas{atlas_index}" + atlas_file = self.fgui_assets.fgui_atlas_dicts[atlas_key] + # 计算在图集中的位置 + x, y = sprite.x, sprite.y + width, height = sprite.width, sprite.height + + # 生成Ren'Py图像定义 + # 由于Ren'Py中文件名带 @ 表示过采样,替换为下划线 _ + atlas_file = atlas_file.replace('@', '_').lower() + image_name = image_name.replace('@', '_') + # 九宫格 + if image_scale == "9grid": + ima_str = f'im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))' + # FGUI中的border是相对x轴和y轴的偏移量,需要根据尺寸再计算为宽度或高度 + left = int(image_scale9grid[0]) + top = int(image_scale9grid[1]) + right = width - left - int(image_scale9grid[2]) + bottom = height - top - int(image_scale9grid[3]) + border_str = f"{left}, {top}, {right}, {bottom}" + image_definitions.append(f'image {image_name} = Frame({ima_str}, {border_str})') + # 平铺 + elif image_scale == "tile": + # image bg tile = Tile("bg.png") + ima_str = f'im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))' + image_definitions.append(f'image {image_name} = Tile({ima_str})') + # 无拉伸普通图片 + else: + image_definitions.append(f'image {image_name} = im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))') + + image_definitions.append("") + self.renpy_code.extend(image_definitions) + + def generate_screen(self, component): + """ + 生成screen定义。目标样例: + + screen test_main_menu(): + add 'menu_bg': + pos (0, 0) + + fixed: + pos (1007, 178) + use main_menu_button(title='开坑', actions=ShowMenu("save")) + fixed: + pos (1007, 239) + use main_menu_button(title='填坑', actions=ShowMenu("load")) + fixed: + pos (1007, 300) + use main_menu_button(title='设置', actions=ShowMenu("preferences")) + fixed: + pos (1007, 361) + use main_menu_button(title='关于', actions=ShowMenu("about")) + fixed: + pos (1007, 422) + use main_menu_button(title='帮助', actions=ShowMenu("help")) + fixed: + pos (1007, 483) + use main_menu_button(title='放弃', actions=Quit()) + """ + self.screen_code.clear() + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + + self.screen_definition_head.append("# 组件Screen") + self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来") + + id = component.id + screen_name = component.name + + self.reset_indent_level() + self.screen_definition_head.append(f"screen {screen_name}():") + self.indent_level_up() + for displayable in component.display_list.displayable_list: + # 图片组件 + if isinstance(displayable, FguiImage): + self.screen_ui_code.extend(self.generate_image_displayable(displayable)) + # 图形组件 + elif isinstance(displayable, FguiGraph): + self.screen_ui_code.extend(self.generate_graph_displayable(displayable)) + # 文本组件 + elif isinstance(displayable, FguiText): + self.screen_ui_code.extend(self.generate_text_displayable(displayable)) + # 列表 + elif isinstance(displayable, FguiList): + self.screen_ui_code.extend(self.generate_list_displayable(displayable)) + # 装载器 + elif isinstance(displayable, FguiLoader): + pass + # 其他组件 + else: + # 根据引用源id查找组件 + ref_com = self.fgui_assets.get_component_by_id(displayable.src) + # 按钮。可设置标题,并根据自定义数据字段设置action。 + if ref_com.extention == "Button" and ref_com.name != None: + self.screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) + self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"") + self.indent_level_down() + + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_variable_code) + self.screen_code.append("") + self.screen_code.extend(self.screen_function_code) + self.screen_code.append("") + # 添加只有1个生效的dismiss + if self.screen_has_dismiss: + self.screen_ui_code.append(f"{self.indent_str}dismiss:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}modal False") + dismiss_action_list = ', '.join(self.dismiss_action_list) + self.screen_ui_code.append(f"{self.indent_str}action [{dismiss_action_list}]") + + self.screen_code.extend(self.screen_ui_code) + + def generate_button_parameter(self, button_title=None, original_actions_str=None): + parameter_str = "" + title_str = "" + actions_str = "" + if button_title : + title_str = f"title=\'{button_title}\'".replace("\n", "\\n").replace("\r", "\\n") + if original_actions_str : + actions_str = f"actions={original_actions_str}" + if button_title and original_actions_str: + parameter_str = f"{title_str}, {actions_str}" + else: + parameter_str = title_str if title_str else actions_str + + return parameter_str + + def generate_text_style(self, fgui_text, style_name): + """ + 生成文本样式,专用于按钮标题,因为按钮中通常只有一个文本组件。 + 目标样例: + + style main_menu_button_text: + align (0.5, 0.5) + font "hyzjhj.ttf" + color "#FEDAAA" + size 32 + outlines [(absolute(1), "#C0C0C0", absolute(3), absolute(3)), (absolute(1), "#FAB5A4", absolute(0), absolute(0))] + textalign 0.5 + """ + + self.style_code.clear() + # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 + if not isinstance(fgui_text, FguiText): + print("It is not a text displayable.") + return + # 样式具有固定一档的缩进 + style_indent = " " + default_title = fgui_text.text + # 定义样式 + self.style_code.append(f"style {style_name}:") + self.style_code.append(f"{style_indent}xysize {fgui_text.size}") + # 字体可能为空,改为Ren'Py内置默认字体SourceHanSansLite + text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" + # 此处的字体名缺少后缀,在Ren'Py中直接使用会报错。 + # 需将字体名添加到font_name_list中,并替换renpy_font_map_definition模板中对应内容。 + if not text_font in self.font_name_list: + self.font_name_list.append(text_font) + self.style_code.append(f"{style_indent}font \"{text_font}\"") + self.style_code.append(f"{style_indent}size {fgui_text.font_size}") + self.style_code.append(f"{style_indent}color \"{fgui_text.text_color}\"") + xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) + self.style_code.append(f"{style_indent}align ({xalign}, {yalign})") + # Ren'Py中使用两层outlines分别实现投影和描边 + # FGUI中的投影包含描边,投影的size等于描边的size,默认值为0;Ren'Py中允许outlines为0,依然有效果 + # Ren'Py中的property outlines可以是列表,先投影后描边 + has_shadow = fgui_text.shadow_color + shadow_width = fgui_text.stroke_size + has_outline = fgui_text.stroke_color + if has_shadow: + shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + if has_outline: + stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + if has_shadow and not has_outline: + self.style_code.append(f"{style_indent}outlines [{shadow_outline}]") + if not has_shadow and has_outline: + self.style_code.append(f"{style_indent}outlines [{stroke_outline}]") + if has_shadow and has_outline: + self.style_code.append(f"{style_indent}outlines [{shadow_outline}, {stroke_outline}]") + # 默认两侧居中对齐 + self.style_code.append(f"{style_indent}textalign 0.5") + # 粗体、斜体、下划线、删除线 + if fgui_text.bold: + self.style_code.append(f"{style_indent}bold {fgui_text.bold}") + if fgui_text.italic: + self.style_code.append(f"{style_indent}italic {fgui_text.italic}") + if fgui_text.underline: + self.style_code.append(f"{style_indent}underline {fgui_text.underline}") + if fgui_text.strike: + self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}") + self.style_code.append("") + + def generate_button_screen(self, component): + + """ + 生成按钮组件screen。目标样例: + + screen main_menu_button(title='', actions=NullAction()): + button: + xysize (273, 61) + style_prefix 'main_menu_button' + background 'main_menu_button_bg' + text title: + align (0.5, 0.5) + action actions + + style main_menu_button_text: + align (0.5, 0.5) + font "hyzjhj.ttf" + color "#FEDAAA" + size 32 + outlines [(absolute(1), "#C0C0C0", absolute(3), absolute(3)), (absolute(1), "#FAB5A4", absolute(0), absolute(0))] + textalign 0.5 + """ + + self.screen_code.clear() + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.has_dismiss = False + self.dismiss_action_list.clear() + self.style_code.clear() + + if component.extention != 'Button': + print("组件类型不是按钮") + return + + # 生成按钮组件screen + self.screen_definition_head.append("# 按钮组件Screen") + self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来") + # screen_code.append("") + + id = component.id + button_name = component.name + xysize = component.size + background = self.default_background + default_title = '' + for displayable in component.display_list.displayable_list: + # 图片组件 + if isinstance(displayable, FguiImage): + for image in self.fgui_assets.package_desc.image_list: + if displayable.src == image.id: + background = image.name + break + # TODO + # 处理不同图片分别用于idle、hover、selected_idle和selected_hover的情况 + # 文本组件 + # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 + # if isinstance(displayable, FguiText) and displayable.name == 'title': + if isinstance(displayable, FguiText): + # 重置缩进级别 + self.reset_indent_level() + default_title = displayable.text + # 定义样式 + self.generate_text_style(displayable, f"{button_name}_text") + # self.screen_code.extend(self.style_code) + + # 重置缩进级别 + self.reset_indent_level() + self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):") + self.indent_level_up() + # 如果按钮有按下效果,添加自定义组件 + if component.button_down_effect: + self.screen_ui_code.append(f"{self.indent_str}button_container:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pressed_{component.button_down_effect} {component.button_down_effect_value}") + self.screen_ui_code.append(f"{self.indent_str}button:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'") + self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})") + self.screen_ui_code.append(f"{self.indent_str}background '{background}'") + self.screen_ui_code.append(f"{self.indent_str}text title") + self.screen_ui_code.append(f"{self.indent_str}action actions") + # 一些按钮特性 + # focus_mask,对应FGUI中的点击“测试”。 + if component.hit_test: + # 根据引用src查找image名 + image_name = self.fgui_assets.get_componentname_by_id(component.hit_test.src) + self.screen_ui_code.append(f"{self.indent_str}focus_mask \'{image_name}\' pos {component.hit_test.pos}") + + self.screen_ui_code.append("") + + self.screen_code.extend(self.style_code) + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + self.renpy_code.extend(self.screen_code) + + def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"): + return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5) + + def generate_image_displayable(self, fgui_image): + """ + 生成图片组件。 + 前提为image对象的定义已经在generate_image_definitions中生成。 + """ + image_code = [] + if not isinstance(fgui_image, FguiImage): + print("It is not a image displayable.") + return image_code + + for image in self.fgui_assets.package_desc.image_list: + if fgui_image.src == image.id: + image_name = image.name + image_code.append(f"{self.indent_str}add \"{image_name}\":") + self.indent_level_up() + image_code.append(f"{self.indent_str}pos {fgui_image.xypos}") + # 必须指定,旋转和缩放都需要使用。 + image_code.append(f"{self.indent_str}anchor {fgui_image.pivot}") + # FairyGUI中锚点固定为(0,0)或与轴心一致,轴心可指定为任意值。 + # Ren'Py中旋转轴心固定为图片中心(0.5,0.5)或与锚点一致,锚点可指定为任意值。 + # 若要与FairyGUI资源保持一致,需设置offset。 + # size可能为None,需要获取 + if not fgui_image.size: + size = self.fgui_assets.get_image_size_by_id(fgui_image.src) + else: + size = fgui_image.size + if not fgui_image.pivot_is_anchor: + xoffset = int(fgui_image.pivot[0] * size[0]) + yoffset = int(fgui_image.pivot[1] * size[1]) + image_code.append(f"{self.indent_str}xoffset {xoffset}") + image_code.append(f"{self.indent_str}yoffset {yoffset}") + else: + image_code.append(f"{self.indent_str}transform_anchor {fgui_image.pivot_is_anchor}") + + if fgui_image.rotation: + image_code.append(f"{self.indent_str}rotate {fgui_image.rotation}") + if fgui_image.alpha != 1.0: + image_code.append(f"{self.indent_str}alpha {fgui_image.alpha}") + if fgui_image.scale != (1.0, 1.0): + image_code.append(f"{self.indent_str}xzoom {fgui_image.scale[0]} yzoom {fgui_image.scale[1]}") + # 九宫格或平铺图片需要指定尺寸 + if image.scale: + image_code.append(f"{self.indent_str}xysize {size}") + self.indent_level_down() + break + return image_code + + def generate_graph_displayable(self, fgui_graph): + """ + 生成图形组件。图形组件有多种类别: + None: 空白 + rect: 矩形(可带圆角) + eclipse: 椭圆(包括圆形) + regular_polygon: 正多边形 + polygon: 多边形 + """ + graph_code = [] + + if not isinstance(fgui_graph, FguiGraph): + print("It is not a graph displayable.") + return graph_code + + # 空白直接使用Null。后续可能存在一些relation相关不匹配。 + if fgui_graph.type is None: + graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}") + # 矩形(圆边矩形)。原生组件不支持圆边矩形,使用自定义shader实现。 + elif fgui_graph.type == "rect": + # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。 + graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\ + .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\ + .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ + .replace('{image_size}', str(fgui_graph.size))\ + .replace('{round_radius}', str(fgui_graph.corner_radius))\ + .replace('{stroke_thickness}', str(fgui_graph.stroke_width)) + # 直接在整个脚本对象中添加graph定义。 + self.renpy_code.append(graph_img_def) + # 椭圆(圆形)。Ren'Py尚不支持椭圆,使用自定义shader实现。 + elif fgui_graph.type == "eclipse": + graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\ + .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\ + .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ + .replace('{image_size}', str(fgui_graph.size))\ + .replace('{stroke_thickness}', str(fgui_graph.stroke_width)) + # 直接在整个脚本对象中添加graph定义。 + self.renpy_code.append(graph_img_def) + + # 生成screen中的部分,带transform。 + graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\":") + self.indent_level_up() + graph_code.append(f"{self.indent_str}xysize {fgui_graph.size}") + graph_code.append(f"{self.indent_str}pos {fgui_graph.xypos}") + graph_code.append(f"{self.indent_str}at transform:") + self.indent_level_up() + graph_code.append(f"{self.indent_str}anchor {fgui_graph.pivot}") + if not fgui_graph.pivot_is_anchor: + size = fgui_graph.size + xoffset = int(fgui_graph.pivot[0] * size[0]) + yoffset = int(fgui_graph.pivot[1] * size[1]) + graph_code.append(f"{self.indent_str}xoffset {xoffset}") + graph_code.append(f"{self.indent_str}yoffset {yoffset}") + graph_code.append(f"{self.indent_str}transform_anchor True") + if fgui_graph.rotation: + graph_code.append(f"{self.indent_str}rotate {fgui_graph.rotation}") + if fgui_graph.alpha != 1.0: + graph_code.append(f"{self.indent_str}alpha {fgui_graph.alpha}") + if fgui_graph.scale != (1.0, 1.0): + graph_code.append(f"{self.indent_str}xzoom {fgui_graph.scale[0]} yzoom {fgui_graph.scale[1]}") + self.indent_level_down() + + self.indent_level_down() + + return graph_code + + def generate_text_displayable(self, fgui_text): + """ + 生成文本组件。非按钮的组件可能存在多个不同文本,不单独生成样式。 + """ + text_code = [] + + # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 + if not isinstance(fgui_text, FguiText): + print("It is not a text displayable.") + return text_code + + # 直接定义text组件。 + # 处理换行符 + text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n") + # 需要根据is_input区分文本组件与输入框 + if fgui_text.is_input: + # Ren'Py中的直接使用input组件无法在多个输入框的情况下切换焦点,也无法点击空白区域让所有输入框失去焦点。 + # 需要使用button作为父组件,与InputValue关联。整个界面添加一个dismiss,空白区域点击事件让输入框失去焦点。 + # pass + # 添加InputValue变量。 + self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name} = '{fgui_text.text}'") + self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name}_input_value = ScreenVariableInputValue('{fgui_text.name}', default=False)") + if self.screen_has_dismiss == False: + self.screen_has_dismiss = True + # 若prompt不为空,需要在screen中添加一个输入检测函数 + if fgui_text.prompt: + self.screen_function_code.append(" python:\n def check_input_length(input_value_object):\n str_length = len(input_value_object.get_text())\n current, editable = renpy.get_editable_input_value()\n return (not editable or current!=input_value_object) and str_length == 0\n") + self.dismiss_action_list.append(f"{fgui_text.name}_input_value.Disable()") + # 用按钮装载input + text_code.append(f"{self.indent_str}button:") + self.indent_level_up() + text_code.append(f"{self.indent_str}action {fgui_text.name}_input_value.Enable()") + self.indent_level_down() + + else: + text_code.append(f"{self.indent_str}text \"{text_str}\":") + self.indent_level_up() + text_code.append(f"{self.indent_str}xysize {fgui_text.size}") + text_code.append(f"{self.indent_str}pos {fgui_text.xypos}") + text_code.append(f"{self.indent_str}at transform:") + self.indent_level_up() + text_code.append(f"{self.indent_str}anchor {fgui_text.pivot}") + if not fgui_text.pivot_is_anchor: + size = fgui_text.size + xoffset = int(fgui_text.pivot[0] * size[0]) + yoffset = int(fgui_text.pivot[1] * size[1]) + text_code.append(f"{self.indent_str}xoffset {xoffset}") + text_code.append(f"{self.indent_str}yoffset {yoffset}") + text_code.append(f"{self.indent_str}transform_anchor True") + if fgui_text.rotation: + text_code.append(f"{self.indent_str}rotate {fgui_text.rotation}") + if fgui_text.alpha != 1.0: + text_code.append(f"{self.indent_str}alpha {fgui_text.alpha}") + if fgui_text.scale != (1.0, 1.0): + text_code.append(f"{self.indent_str}xzoom {fgui_text.scale[0]} yzoom {fgui_text.scale[1]}") + self.indent_level_down() + + # input组件额外内容 + if fgui_text.is_input: + # 若有prompt,添加一个带显示条件的text。 + if fgui_text.prompt: + prompt_str = fgui_text.prompt.replace("\n", "\\n").replace("\r", "\\n") + text_code.append(f"{self.indent_str}showif check_input_length({fgui_text.name}_input_value):") + self.indent_level_up() + text_code.append(f"{self.indent_str}text \"{prompt_str}\":") + self.indent_level_up() + # 暂时使用与input相同样式 + text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" + if not text_font in self.font_name_list: + self.font_name_list.append(text_font) + text_code.append(f"{self.indent_str}font \"{text_font}\"") + text_code.append(f"{self.indent_str}size {fgui_text.font_size}") + text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"") + has_shadow = fgui_text.shadow_color + shadow_width = fgui_text.stroke_size + has_outline = fgui_text.stroke_color + if has_shadow: + shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + if has_outline: + stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + if has_shadow and not has_outline: + text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") + if not has_shadow and has_outline: + text_code.append(f"{self.indent_str}outlines [{stroke_outline}]") + if has_shadow and has_outline: + text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]") + if fgui_text.letter_spacing: + text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}") + if fgui_text.leading: + text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}") + xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) + text_code.append(f"{self.indent_str}textalign {xalign}") + if fgui_text.bold: + text_code.append(f"{self.indent_str}bold {fgui_text.bold}") + if fgui_text.italic: + text_code.append(f"{self.indent_str}italic {fgui_text.italic}") + if fgui_text.underline: + text_code.append(f"{self.indent_str}underline {fgui_text.underline}") + if fgui_text.strike: + text_code.append(f"{self.indent_str}strikethrough {fgui_text.strike}") + self.indent_level_down(leveldown=2) + + + text_code.append(f"{self.indent_str}input:") + self.indent_level_up() + text_code.append(f"{self.indent_str}value {fgui_text.name}_input_value") + + # 字体可能为空,改为Ren'Py内置默认字体SourceHanSansLite + text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" + # 此处的字体名缺少后缀,在Ren'Py中直接使用会报错。 + # 需将字体名添加到font_name_list中,并替换renpy_font_map_definition模板中对应内容。 + if not text_font in self.font_name_list: + self.font_name_list.append(text_font) + text_code.append(f"{self.indent_str}font \"{text_font}\"") + text_code.append(f"{self.indent_str}size {fgui_text.font_size}") + text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"") + # Ren'Py中使用两层outlines分别实现投影和描边 + # FGUI中的投影包含描边,投影的size等于描边的size,默认值为0;Ren'Py中允许outlines为0,依然有效果。 + # Ren'Py中的property outlines是列表,先投影后描边。 + has_shadow = fgui_text.shadow_color + shadow_width = fgui_text.stroke_size + has_outline = fgui_text.stroke_color + if has_shadow: + shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + if has_outline: + stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + if has_shadow and not has_outline: + text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") + if not has_shadow and has_outline: + text_code.append(f"{self.indent_str}outlines [{stroke_outline}]") + if has_shadow and has_outline: + text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]") + # 字间距与行距 + if fgui_text.letter_spacing: + text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}") + if fgui_text.leading: + text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}") + + # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置 + xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) + text_code.append(f"{self.indent_str}textalign {xalign}") + # 粗体、斜体、下划线、删除线 + if fgui_text.bold: + text_code.append(f"{self.indent_str}bold {fgui_text.bold}") + if fgui_text.italic: + text_code.append(f"{self.indent_str}italic {fgui_text.italic}") + if fgui_text.underline: + text_code.append(f"{self.indent_str}underline {fgui_text.underline}") + if fgui_text.strike: + text_code.append(f"{self.indent_str}strikethrough {fgui_text.strike}") + # 输入框特有properties + if fgui_text.is_input: + text_code.append(f"{self.indent_str}pixel_width {fgui_text.size[0]}") + if not fgui_text.single_line: + text_code.append(f"{self.indent_str}multiline True") + if fgui_text.max_length: + text_code.append(f"{self.indent_str}length {fgui_text.max_length}") + if fgui_text.is_password: + text_code.append(f"{self.indent_str}mask '*'") + # FGUI的输入限制使用正则表达式。在Ren'Py中使用字符串。此处仅为占位,无效果。 + text_code.append(f"{self.indent_str}allow {{}}") + text_code.append(f"{self.indent_str}exclude {{}}") + if fgui_text.is_input: + self.indent_level_down() + + self.indent_level_down() + return text_code + + def generate_list_displayable(self, fgui_list): + """ + 生成列表。 + """ + list_code = [] + if not isinstance(fgui_list, FguiList): + print("It is not a list displayable.") + return list_code + + # 默认引用组件可能是图片或其他组件,后续处理方式不同。 + default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id) + default_item_type = None + default_item_name = None + item_number = len(fgui_list.item_list) + # 若为组件 + if default_item: + default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id) + default_item_type = 'component' + # 若为图片 + else: + default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id) + if default_item_name: + default_item_type = 'image' + else: + print("Ref com not found.") + return list_code + + # 根据“溢出处理”是否可见区分处理。 + # 若“可见”,则使用hbox、vbox和grid。 + if fgui_list.overflow == "visible": + # 单列竖排,使用vbox + if fgui_list.layout == "column": + list_code.append(f"{self.indent_str}vbox:") + # 单行横排,使用hbox + elif fgui_list.layout == "row": + list_code.append(f"{self.indent_str}hbox:") + # 其他,使用grid + else: + list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:") + + else: + list_code.append(f"{self.indent_str}vpgrid:") + self.indent_level_up() + + # 若“隐藏”,使用不可滚动的vpgrid + if fgui_list.overflow == "hidden": + list_code.append(f"{self.indent_str}draggable False") + # 单列竖排 + if fgui_list.layout == "column": + cols = 1 + rows = item_number + # 单行横排 + elif fgui_list.layout == "row": + cols = item_number + rows = 1 + # 其他 + else: + cols = fgui_list.line_item_count + rows = fgui_list.line_item_count2 + list_code.append(f"{self.indent_str}cols {cols}") + list_code.append(f"{self.indent_str}rows {rows}") + # 若“滚动”,使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。 + elif fgui_list.overflow == "scroll": + list_code.append(f"{self.indent_str}draggable True") + # 垂直滚动 + if fgui_list.scroll == "vertical": + pass + # 水平滚动 + elif fgui_list.scroll == "horizontal": + pass + # 自由滚动 + elif fgui_list.scroll == "both": + pass + # 单列竖排,使用vbox + if fgui_list.layout == "column": + cols = 1 + rows = item_number + # 单行横排,使用hbox + elif fgui_list.layout == "row": + cols = item_number + rows = 1 + # 其他,使用grid + else: + cols = fgui_list.line_item_count + rows = fgui_list.line_item_count2 + list_code.append(f"{self.indent_str}cols {cols}") + list_code.append(f"{self.indent_str}rows {rows}") + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}") + if fgui_list.col_gap: + list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}") + self.indent_level_down() + + self.indent_level_up() + list_code.append(f"{self.indent_str}pos {fgui_list.xypos}") + list_code.append(f"{self.indent_str}xysize {fgui_list.size}") + if fgui_list.margin: + list_code.append(f"{self.indent_str}margin {fgui_list.margin}") + # 添加元素 + for item in fgui_list.item_list: + # 非默认元素 + if item.item_url: + pass + # 默认元素 + else: + if default_item_type == "image": + list_code.append(f"{self.indent_str}add \'{default_item_name}\'") + elif default_item_type == "component": + parameter_str = self.generate_button_parameter(item.item_title) + list_code.append(f"{self.indent_str}use {default_item_name}({parameter_str})") + + self.indent_level_down() + return list_code + + def generate_renpy_code(self): + """生成完整的Ren'Py代码""" + self.renpy_code = [] + + # 添加文件头注释 + self.renpy_code.append("# -*- coding: utf-8 -*-") + self.renpy_code.append("#") + self.renpy_code.append("# 从FairyGUI项目转换的Ren'Py界面代码") + self.renpy_code.append(f"# 资源包名: {self.fgui_assets.fgui_project_name}") + self.renpy_code.append("#") + self.renpy_code.append("") + + # 生成图像定义 + self.generate_image_definitions() + + for component in self.fgui_assets.fgui_component_set: + if component.extention == 'Button': + self.generate_button_screen(component) + elif component.extention == 'ScrollBar': + pass + elif component.extention == 'Label': + pass + elif component.extention == 'Slider': + pass + elif component.extention == 'ComboBox': + pass + elif component.extention == 'ProgressBar': + pass + else: + self.generate_screen(component) + self.renpy_code.extend(self.screen_code) + + def save_to_file(self, filename): + """ + 保存Ren'Py代码 + """ + with open(filename, 'w', encoding='utf-8') as f: + for line in self.renpy_code: + f.write(line + '\n') + + print(f"Ren'Py代码已保存到: {filename}") + + def from_templates_to_renpy(self, filename): + """ + 读取模板替换字符串并保存至Ren'Py目录 + """ + # 字体字典 + with open(os.path.join(self.renpy_template_dir, self.font_map_template), 'r', encoding='utf-8') as file: + content = file.read() + font_name_list_str = ','.join(f'"{i}"' for i in self.font_name_list) + content = content.replace("{font_name_list}", font_name_list_str) + + with open(filename, 'w', encoding='utf-8') as f: + f.write(content) + + def get_graph_template(self, filename): + """ + 获取graph对应的image对象定义模板 + """ + with open(os.path.join(self.renpy_template_dir, filename), 'r', encoding='utf-8') as file: + content = file.read() + return content + + + def copy_predefine_files(self, source_dir, target_dir): + """ + 复制预定义cdd和cds的文件 + """ + print(f"source_dir: {source_dir}") + # 所有rpy文件 + all_files = os.listdir(source_dir) + for file in all_files: + if file.endswith('.rpy'): + filename = os.path.basename(file) + source_file_path = os.path.join(source_dir, filename) + target_file_path = os.path.join(target_dir, filename) + shutil.copy2(source_file_path,target_file_path) + + + def copy_atlas_files(self, source_dir, target_dir): + """复制图集文件到目标目录,并将@替换为_""" + + # 所有图集文件 + atlas_files = self.fgui_assets.fgui_atlas_dicts.values() + + # 复制文件并重命名 + for atlas_file in atlas_files: + # 将@替换为_ + new_filename = atlas_file.replace('@', '_') + source_path = os.path.join(source_dir, atlas_file) + target_path = os.path.join(target_dir, new_filename) + shutil.copy2(source_path, target_path) + print(f"✓ 复制并重命名图集文件: {atlas_file} -> {new_filename}") + + return len(atlas_files) + +def convert(argv): + """ + 主函数:解析FguiDemoPackage并转换为Ren'Py代码 + """ + # 创建命令行参数解析器 + parser = argparse.ArgumentParser(description='FairyGUI资源到Ren\'Py界面脚本的转换器', + epilog='使用示例:\n' + ' python Fgui2RenpyConverter.py -i "F:\\FguiDemoPackage" -o "F:\\RenpyProjects\\MyGame"\n' + ' python Fgui2RenpyConverter.py --input "MyFGUIAssetPackage" --output "/path/renpy/project"', + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('-i', '--input', type=str, + help='输入FairyGUI资源文件所在目录名 (目录中需存在同名 .bytes 文件)') + parser.add_argument('-o', '--output', type=str, + help='输出Ren\'Py项目基目录路径 (即Ren\'Py项目根目录)') + + # 解析命令行参数 + args = parser.parse_args(argv[1:] if argv and len(argv) > 1 else []) + + # 检查必需的参数 + if not args.input: + print("错误: 必须指定输入目录 (-i 或 --input)") + parser.print_help() + return + + if not args.output: + print("错误: 必须指定输出目录 (-o 或 --output)") + parser.print_help() + return + + fgui_project_path = args.input + if os.path.exists(fgui_project_path) and os.path.isdir(fgui_project_path): + fgui_project_name = os.path.basename(fgui_project_path) + else: + print(f"错误: 目录 {fgui_project_path} 不存在或不是有效目录") + return + + renpy_base_dir = args.output + + print("开始将FairyGUI资源文件转换为Ren'Py脚本...") + print("=" * 50) + + try: + + # 检查文件是否存在 + package_file = f"{fgui_project_path}/{fgui_project_name}.bytes" + sprite_file = f"{fgui_project_path}/{fgui_project_name}@sprites.bytes" + + if not os.path.exists(package_file): + print(f"错误: 找不到文件 {package_file}") + return + + if not os.path.exists(sprite_file): + print(f"错误: 找不到文件 {sprite_file}") + return + + # 创建Ren'Py游戏基础目录结构 + game_dir = os.path.join(renpy_base_dir, "game") + images_dir = os.path.join(game_dir, "images") + scripts_dir = os.path.join(game_dir, "scripts") + + # 创建目录 + os.makedirs(game_dir, exist_ok=True) + os.makedirs(images_dir, exist_ok=True) + os.makedirs(scripts_dir, exist_ok=True) + print(f"创建目录结构: {renpy_base_dir}/") + print(f"├── game/") + print(f"└── game/images/") + print(f"└── game/scripts/") + + # 创建FguiAssets对象 + print("\n正在解析FairyGUI资源...") + fgui_assets = FguiAssets(fgui_project_path) + print("FairyGUI资源解析完成") + + # 创建转换器 + print("正在创建转换器...") + converter = FguiToRenpyConverter(fgui_assets) + print("转换器创建完成") + + # 生成Ren'Py代码 + print("正在生成Ren'Py代码...") + converter.generate_renpy_code() + print("Ren'Py代码生成完成") + + # 保存.rpy文件到game目录 + output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy") + converter.save_to_file(output_file) + + # 部分预定义模板文件修改参数并保存 + font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy") + converter.from_templates_to_renpy(font_map_definition_file) + + # 复制预定义cdd和cds文件 + converter.copy_predefine_files(converter.renpy_template_dir, scripts_dir) + + # 复制图集文件到images目录 + print("\n正在复制图集文件...") + current_dir = os.getcwd() + atlas_count = converter.copy_atlas_files(fgui_project_path, images_dir) + print(f"复制了 {atlas_count} 个图集文件") + + + except Exception as e: + print(f"❌ 转换过程中出现错误: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + convert(sys.argv) \ No newline at end of file diff --git a/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy b/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy new file mode 100644 index 0000000..d2356a5 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy @@ -0,0 +1,175 @@ +python early: + + import pygame + import math + + class ButtonContainer(renpy.display.behavior.Button): + """ + 按钮容器类,按下后有缩放和改变颜色(未实现)效果。 + """ + def __init__(self, pressed_scale=1.0, pressed_dark=1.0, *args, **kwargs): + super(ButtonContainer, self).__init__(**kwargs) + self.pressed_scale = pressed_scale + # FGUI中变暗的取值范围为0~1,0完全黑,1完全无效果。(编辑器中允许输入值超过1,但无效果。) + # 此处使用BrightnessMatrix类,入参取值范围-1~1,-1完全变黑,0完全无效果,1完全变白。 + # 因此需要做一个转换 + self.pressed_dark = min(pressed_dark, 1.0) - 1.0 + self.brightness_matrix = BrightnessMatrix(value=self.pressed_dark) + self.button_pressed = False + self.width = 0 + self.height = 0 + self.blit_pos = (0, 0) + + def render(self, width, height, st, at): + if self.button_pressed and self.pressed_dark != 0: + t = Transform(child=self.child, anchor=(0.5, 0.5), matrixcolor=self.brightness_matrix) + else: + t = Transform(child=self.child, anchor=(0.5, 0.5), matrixcolor=None) + child_render = renpy.render(t, width, height, st, at) + self.width, self.height = child_render.get_size() + self.size = (self.width, self.height) + render = renpy.Render(self.width, self.height) + if self.button_pressed: + if self.pressed_scale != 1.0: + child_render.zoom(self.pressed_scale, self.pressed_scale) + # 为了居中,重新计算blit坐标 + self.blit_pos = ((int)(self.width*(1-self.pressed_scale)/2), (int)(self.height*(1-self.pressed_scale)/2)) + else: + self.blit_pos = (0, 0) + render.blit(child_render, self.blit_pos) + return render + + def event(self, ev, x, y, st): + if renpy.map_event(ev, "mousedown_1") and renpy.is_pixel_opaque(self.child, self.width, self.height, st=st, at=0, x=x, y=y) and not self.button_pressed: + self.button_pressed = True + renpy.redraw(self, 0) + return self.child.event(ev, x, y, st) + if self.button_pressed: + if renpy.map_event(ev, "mouseup_1"): + self.button_pressed = False + renpy.redraw(self, 0) + elif ev.type == pygame.MOUSEMOTION and ev.buttons[0] != 1 : + self.button_pressed = False + renpy.redraw(self, 0) + return self.child.event(ev, x, y, st) + + def visit(self): + return [ self.child ] + +python early: + renpy.register_sl_displayable("button_container", ButtonContainer, "pressed_button", 1)\ + .add_property("pressed_scale")\ + .add_property("pressed_dark")\ + .add_property_group("button") + +init python: + class SquenceAnimator(renpy.Displayable): + """ + 多图序列帧动画组件。 + """ + def __init__(self, prefix, separator, begin_index, end_index, interval, loop=True, **kwargs): + super(SquenceAnimator, self).__init__(**kwargs) + self.prefix = prefix + self.separator = separator + self.begin_index = begin_index + self.end_index = end_index + self.length = end_index - begin_index + 1 + + + self.sequence = [] + for i in range(begin_index, end_index+1): + self.sequence.append(renpy.displayable(self.prefix + self.separator + str(i))) + + self.current_index = 0 + self.show_timebase = 0 + + self.interval = interval + self.loop = loop + + def render(self, width, height, st, at): + ## st为0时,表示组件重新显示 + if st == 0: + self.show_timebase = 0 + self.current_index = 0 + if (st >= (self.show_timebase + self.interval)): + self.show_timebase = st + self.current_index += 1 + if self.current_index >= self.length: + if self.loop: + self.current_index = 0 + else: + self.current_index = self.length - 1 + + render = renpy.render(self.sequence[self.current_index], width, height, st, at) + renpy.redraw(self, 0) + + return render + + # 重置序列帧 + def reset_sequence_index(self): + self.current_index = 0 + + def get_frame_image(self, index): + return self.sequence[index] + + class SquenceAnimator2(renpy.Displayable): + """ + 单图序列帧动画组件。 + """ + def __init__(self, img, row, column, interval, loop=True, **kwargs): + + super(SquenceAnimator2, self).__init__(**kwargs) + # im入参是字符串,需要转为Image对象,获取尺寸信息 + self.img = Image(img) + self.size = renpy.image_size(self.img) + # 行数 + self.row = row + # 列数 + self.column = column + # 单帧宽度 + self.frame_width = int(self.size[0] / column) + # 单帧高度 + self.frame_height = int(self.size[1] / row) + # 序列帧长度 + self.length = row * column + + self.sequence = [] + # 循环嵌套切割单帧图像 + for i in range(row): + for j in range(column): + # im.Crop()已被标记为deprecated,但剪裁边缘正确。 + # Crop()方法在右、低两边会有错误。 + # 参考 https://github.com/renpy/renpy/issues/6376 + self.sequence.append(im.Crop(self.img, (self.frame_width*j, self.frame_height*i, self.frame_width, self.frame_height))) + + self.current_index = 0 + self.show_timebase = 0 + + self.interval = interval + self.loop = loop + + def render(self, width, height, st, at): + ## st为0时,表示组件重新显示 + if st == 0: + self.show_timebase = 0 + self.current_index = 0 + if (st >= (self.show_timebase + self.interval)): + self.show_timebase = st + self.current_index += 1 + if self.current_index >= self.length: + if self.loop: + self.current_index = 0 + else: + self.current_index = self.length - 1 + + render = renpy.render(self.sequence[self.current_index], width, height, st, at) + renpy.redraw(self, 0) + + return render + + # 重置序列帧 + def reset_sequence_index(self): + self.current_index = 0 + + def get_frame_image(self, index): + return self.sequence[index] diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy new file mode 100644 index 0000000..c14bf28 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy @@ -0,0 +1,99 @@ +init python: + + renpy.register_shader("CursedOctopus.rectangle", variables=""" + uniform vec4 u_rectangle_color; + uniform vec4 u_stroke_color; + uniform vec2 u_model_size; + uniform float u_radius; + uniform float u_thickness; + attribute vec2 a_tex_coord; + varying vec2 v_tex_coord; + """, vertex_300=""" + v_tex_coord = a_tex_coord; + """,fragment_functions=""" + float roundedBoxSDF(vec2 pos, vec2 border, float radius){ + vec2 dis = abs(pos) - border + vec2(radius,radius); + return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius; + } + """,fragment_300=""" + vec2 uv = v_tex_coord - vec2(0.5, 0.5); + vec2 tex_pos = uv * u_model_size; + float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius); + float border_alpha = (1.0 - step(0.0, out_distance)) * u_stroke_color.a; + float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius); + float fill_alpha = (1.0 - step(0.0, in_distance)) * u_rectangle_color.a; + vec4 c1 = step(1-fill_alpha, 0) * u_rectangle_color; + vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color; + gl_FragColor = c1 + c2; + """) + + renpy.register_shader("CursedOctopus.rectangleAA", variables=""" + uniform vec4 u_rectangle_color; + uniform vec4 u_stroke_color; + uniform vec2 u_model_size; + uniform float u_radius; + uniform float u_thickness; + uniform float u_edge_softness; + attribute vec2 a_tex_coord; + varying vec2 v_tex_coord; + """, vertex_300=""" + v_tex_coord = a_tex_coord; + """,fragment_functions=""" + float roundedBoxSDF(vec2 pos, vec2 border, float radius){ + vec2 dis = abs(pos) - border + vec2(radius,radius); + return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius; + } + """,fragment_300=""" + vec2 uv = v_tex_coord - vec2(0.5, 0.5); + vec2 tex_pos = uv * u_model_size; + float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius); + float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance)) * u_stroke_color.a; + float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius); + float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance)) * u_rectangle_color.a; + vec4 c1 = fill_alpha * u_rectangle_color; + vec4 c2 = border_alpha * u_stroke_color; + gl_FragColor = mix(c2, c1, fill_alpha); + """) + + renpy.register_shader("CursedOctopus.ellipse", variables=""" + uniform vec4 u_ellipse_color; + uniform vec4 u_stroke_color; + uniform vec2 u_model_size; + uniform float u_thickness; + attribute vec2 a_tex_coord; + varying vec2 v_tex_coord; + """, vertex_300=""" + v_tex_coord = a_tex_coord; + """,fragment_300=""" + vec2 uv = v_tex_coord - vec2(0.5, 0.5); + float out_distance = length(uv); + float border_alpha = step(out_distance, 0.5); + vec2 tex_pos = uv * u_model_size; + float in_distance = length((abs(tex_pos+normalize(uv*u_thickness)*u_thickness))/u_model_size); + float fill_alpha = step(in_distance, 0.5); + vec4 c1 = step(1-fill_alpha, 0) * u_ellipse_color; + vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color; + gl_FragColor = c1 + c2; + """) + + renpy.register_shader("CursedOctopus.ellipseAA", variables=""" + uniform vec4 u_ellipse_color; + uniform vec4 u_stroke_color; + uniform vec2 u_model_size; + uniform float u_thickness; + uniform float u_edge_softness; + attribute vec2 a_tex_coord; + varying vec2 v_tex_coord; + """, vertex_300=""" + v_tex_coord = a_tex_coord; + """,fragment_300=""" + vec2 uv = v_tex_coord - vec2(0.5, 0.5); + float out_distance = length(uv); + float border_alpha = smoothstep(out_distance-u_edge_softness, out_distance, 0.5-u_edge_softness); + vec2 tex_pos = uv * u_model_size; + float in_distance = length((abs(tex_pos+normalize(uv*u_thickness)*u_thickness))/u_model_size); + float fill_alpha = smoothstep(in_distance-u_edge_softness, in_distance, 0.5-u_edge_softness); + vec4 c1 = fill_alpha * u_ellipse_color; + vec4 c2 = border_alpha * u_stroke_color; + gl_FragColor = mix(c2, c1, fill_alpha); + """) diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt new file mode 100644 index 0000000..1f77827 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt @@ -0,0 +1,2 @@ +# 抗锯齿的椭圆(圆形)图像定义 +image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01) diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt new file mode 100644 index 0000000..8a36cb8 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt @@ -0,0 +1,2 @@ +# 椭圆(圆形)图像定义 +image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}) diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt new file mode 100644 index 0000000..3255979 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt @@ -0,0 +1,14 @@ +init python: + import os + font_name_list = [{font_name_list}] + font_file_ext = [".ttf", ".ttc", ".otf"] + + for font_name in font_name_list: + for font_ext in font_file_ext: + font_file_name = f"{font_name}{font_ext}" + if os.path.exists(f"{config.gamedir}\\{font_file_name}") or os.path.exists(f"{config.gamedir}\\fonts\\{font_file_name}"): + config.font_name_map[font_name] = font_file_name + renpy.log(config.font_name_map) + break + else: + print(f"Could not Find font: {font_name}") diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt new file mode 100644 index 0000000..e54000c --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt @@ -0,0 +1,2 @@ +# 带光滑渐变的圆角矩形图像定义 +image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0) diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt new file mode 100644 index 0000000..9dab793 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt @@ -0,0 +1,2 @@ +# 圆角矩形图像定义 +image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}) diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py index 2d91e18..bdacc97 100644 --- a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py +++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py @@ -9,6 +9,8 @@ from ..translatablewidgetinterface import * from ..util.fileopen import FileOpenHelper +from fgui_converter.utils.renpy.Fgui2RenpyConverter import * + TR_gui_fguiconverter = TranslationDomain("gui_fguiconverter") class FguiConverterWidget(QWidget, ToolWidgetInterface): @@ -18,7 +20,7 @@ class FguiConverterWidget(QWidget, ToolWidgetInterface): verifyCB : typing.Callable[[str], bool] | None fieldName : Translatable | str filter : Translatable | str - lastAddedPath : str + lastInputPath : str lastOutputPath : str ui : Ui_FguiConverterWidget @@ -74,7 +76,7 @@ def __init__(self, parent : QWidget): self.verifyCB = None self.fieldName = "" self.filter = "" - self.lastAddedPath = "" + self.lastInputPath = "" self.lastOutputPath = "" self.bind_text(self.ui.fguiAssetDictLabel.setText, self._tr_fgui_assets_dict_label) @@ -93,6 +95,7 @@ def __init__(self, parent : QWidget): self.setAcceptDrops(True) self.ui.outputDictButton.clicked.connect(self.itemOpenOutputDirectory) self.ui.outputDictLine.textChanged.connect(self.setOutputDict) + self.ui.convertButton.clicked.connect(self.generateRenpyUi) @classmethod def getToolInfo(cls, **kwargs) -> ToolWidgetInfo: @@ -154,9 +157,9 @@ def addPath(self, path: str): if self.ui.listWidget.item(i).text() == path: return newItem = QListWidgetItem(path) - newItem.setToolTip(path) + #newItem.setToolTip(path) self.ui.listWidget.addItem(newItem) - self.lastAddedPath = path + self.lastInputPath = path self.listChanged.emit() @Slot() @@ -193,7 +196,7 @@ def itemOpenContainingDirectory(self): @Slot() def itemAdd(self): dialogTitle = self._tr_select_dialog_title.format(field=str(self.fieldName)) - initialDir = self.lastAddedPath if self.lastAddedPath else "" + initialDir = self.lastInputPath if self.lastInputPath else "" dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter)) if self.isDirectoryMode: dialog.setFileMode(QFileDialog.Directory) @@ -235,10 +238,21 @@ def setOutputDict(self, path: str): self.ui.outputDictLine.setText(path) self.lastOutputPath = path - @classmethod - def getToolInfo(cls, **kwargs) -> ToolWidgetInfo: - return ToolWidgetInfo( - idstr="guiconverter", - name=MainWindowInterface.tr_toolname_guiconverter, - widget=cls, - ) + @Slot() + def generateRenpyUi(self): + # 检查当前输入输出设置。 + # 报错暂时只在Python命令行打印,后续改为弹窗信息。 + if self.ui.listWidget.count() > 0: + inputPathList = self.getCurrentList() + else: + print("Input Path List is Empty.") + return + outputPathStr = self.ui.outputDictLine.text() + if os.path.isdir(outputPathStr): + print(outputPathStr) + else: + print("Ren'Py Project base dictionary does not exsit.") + return + + # 创建FguiToRenpyConverter对象 + convert(["Test", "-i", inputPathList[0], "-o", outputPathStr]) \ No newline at end of file From 73b3cc5d50d277e190c4adbcc04c434e2a81a737 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Fri, 17 Oct 2025 10:11:27 +0800 Subject: [PATCH 03/19] Add independency in setup.cfg --- .gitignore | 2 ++ setup.cfg | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e642b8f..dfe4d74 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,5 @@ src/preppipe_gui_pyside6/forms/generated # version files for pyinstaller build versionfile_*.txt +run_cli.py +run_gui.py diff --git a/setup.cfg b/setup.cfg index ffe922b..8db1325 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ install_requires = xlsxwriter psd-tools antlr4-python3-runtime >= 4.10, < 4.11.0 + lxml>=4.9.0 # GUI extra dependencies here # https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#configuring-setup-using-setup-cfg-files From 02a7c36df024811268f72d2ebe324704b3878103 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Fri, 17 Oct 2025 10:47:33 +0800 Subject: [PATCH 04/19] Modify README.md, add `-o` option to `pyside6-uic` command. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2e99bf..fdff3c0 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ git config --local include.path $PWD/gitconfig 需要手动执行的有:(请在上述开发环境配置完毕后执行) * 资源文件处理。请在获取[资源仓](#素材资源-assets)后,在仓库根目录下运行 `python3 ./build_assets.py` 以生成 `src/preppipe/assets/_install` 下的内容。该操作需要在资源列表更新时或任意资源类型保存的的内部数据结构改变时重新进行。 -* GUI 中 PySide6 `.ui` 文件编译。请在 `src/preppipe_gui_pyside6/forms` 目录下将所有诸如 `xxx.ui` 的文件使用命令 `pyside6-uic xxx.ui generated/ui_xxx.py` 编译成 `.py`。如果您使用 Linux,您可以直接用该目录下的 `Makefile`。该操作需要在任意 .ui 文件更改后重新执行。 +* GUI 中 PySide6 `.ui` 文件编译。请在 `src/preppipe_gui_pyside6/forms` 目录下将所有诸如 `xxx.ui` 的文件使用命令 `pyside6-uic xxx.ui -o generated/ui_xxx.py` 编译成 `.py`。如果您使用 Linux,您可以直接用该目录下的 `Makefile`。该操作需要在任意 .ui 文件更改后重新执行。 ## GUI启动 From 0f867cea79350c02c18a0040505899c9f1445870 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Sat, 18 Oct 2025 17:05:37 +0800 Subject: [PATCH 05/19] Add null "input/output" popup and fix some Converter bug 1. PrepPipe-gui support specifying item within multi input packages. 2. Popup message box if input/output is empty. 3. Converter Module add or fix delete or clean methods. --- src/fgui_converter/FguiAssetsParseLib.py | 7 ++++ .../utils/renpy/Fgui2RenpyConverter.py | 32 +++++++++++++++++++ .../toolwidgets/fguiconverter.py | 27 ++++++++++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index a5af312..9d28fbb 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -997,10 +997,14 @@ def __init__(self, fgui_project_path): def clear(self): self.fgui_project_name = '' + self.package_desc_file = '' + self.sprite_desc_file = '' self.package_desc.clear() self.object_dict.clear() self.fgui_atlas_dicts.clear() self.fgui_component_set.clear() + self.fgui_image_set.clear() + self.fgui_atlas_dicts.clear() def get_componentname_by_id(self, id): return self.package_desc.id_name_mapping[id] @@ -1015,3 +1019,6 @@ def get_image_size_by_id(self, id): for image in self.fgui_image_set: if image.image_id == id: return (image.width, image.height) + + def __del__(self): + self.clear() diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 0508eff..c1c60d3 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -855,6 +855,35 @@ def get_graph_template(self, filename): content = file.read() return content + def cleanup(self): + """ + 清理转换器资源 + 清理内存中的数据结构、模板缓存等资源 + """ + try: + self.fgui_assets = None + # 清理代码生成相关的列表 + self.renpy_code.clear() + self.screen_code.clear() + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.style_code.clear() + self.dismiss_action_list.clear() + + # 重置缩进相关状态 + self.root_indent_level = 0 + self.indent_str = '' + self.screen_has_dismiss = False + + except Exception as e: + print(f"清理资源时出现错误: {e}") + + def __del__(self): + self.cleanup() + + def copy_predefine_files(self, source_dir, target_dir): """ @@ -989,6 +1018,9 @@ def convert(argv): atlas_count = converter.copy_atlas_files(fgui_project_path, images_dir) print(f"复制了 {atlas_count} 个图集文件") + # 一些清理 + fgui_assets.clear() + converter.cleanup() except Exception as e: print(f"❌ 转换过程中出现错误: {e}") diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py index bdacc97..4d866df 100644 --- a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py +++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py @@ -238,6 +238,24 @@ def setOutputDict(self, path: str): self.ui.outputDictLine.setText(path) self.lastOutputPath = path + _tr_unable_to_transform = TR_gui_fguiassetsdictwidget.tr("unable_to_transform", + en="Unable to transform", + zh_cn="无法转换", + zh_hk="無法轉換", + ) + + _tr_input_required = TR_gui_fguiassetsdictwidget.tr("input_required", + en="Please specify input directory first", + zh_cn="请先指定输入文件夹", + zh_hk="請先指定輸入文件夾", + ) + + _tr_output_required = TR_gui_fguiassetsdictwidget.tr("output_required", + en="Please specify output directory first", + zh_cn="请先指定输出文件夹", + zh_hk="請先指定輸出文件夾", + ) + @Slot() def generateRenpyUi(self): # 检查当前输入输出设置。 @@ -246,13 +264,18 @@ def generateRenpyUi(self): inputPathList = self.getCurrentList() else: print("Input Path List is Empty.") + QMessageBox.critical(self, self._tr_unable_to_transform.get(), self._tr_input_required.get()) return outputPathStr = self.ui.outputDictLine.text() if os.path.isdir(outputPathStr): print(outputPathStr) else: print("Ren'Py Project base dictionary does not exsit.") + QMessageBox.critical(self, self._tr_unable_to_transform.get(), self._tr_output_required.get()) return - # 创建FguiToRenpyConverter对象 - convert(["Test", "-i", inputPathList[0], "-o", outputPathStr]) \ No newline at end of file + curRow = self.ui.listWidget.currentRow() + if curRow >= 0: + #item = self.ui.listWidget.takeItem(curRow) + # 创建FguiToRenpyConverter对象 + convert(["Test", "-i", inputPathList[curRow], "-o", outputPathStr]) \ No newline at end of file From 743a764be9e4dfe64e6ab55164af0cb38acfb973 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Tue, 4 Nov 2025 18:30:19 +0800 Subject: [PATCH 06/19] Restructured button converter method 1. Restructuring of button converter method. 2. Update graph templates. --- .../utils/renpy/Fgui2RenpyConverter.py | 337 +++++++++++++++--- .../renpy_ellipseAA_template.txt | 13 +- .../renpy_ellipse_template.txt | 13 +- .../renpy_templates/renpy_null_template.txt | 2 + .../renpy_rectangleAA_template.txt | 13 +- .../renpy_rectangle_template.txt | 13 +- 6 files changed, 331 insertions(+), 60 deletions(-) create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index c1c60d3..72024c0 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -7,6 +7,15 @@ import argparse import shutil +from enum import IntEnum + +class DisplayableChildType(IntEnum): + NULL = 0 + IMAGE = 1 + GRAPH = 2 + TEXT = 3 + COMPONENT = 4 + OTHER = 5 # 添加当前目录到Python路径,以便导入FguiAssetsParseLib sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -55,6 +64,7 @@ def __init__(self, fgui_assets): os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates")) self.font_map_template = 'renpy_font_map_definition.txt' self.graph_template_dict = {} + self.graph_template_dict['null'] = self.get_graph_template('renpy_null_template.txt') self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt') self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt') @@ -130,6 +140,79 @@ def generate_image_definitions(self): image_definitions.append("") self.renpy_code.extend(image_definitions) + def generate_graph_definitions(self, fgui_graph): + """ + 生成图形组件定义。图形组件有多种类别: + None: 空白 + rect: 矩形(可带圆角) + eclipse: 椭圆(包括圆形) + regular_polygon: 正多边形 + polygon: 多边形 + """ + graph_code = [] + graph_img_def = '' + + if not isinstance(fgui_graph, FguiGraph): + print("It is not a graph displayable.") + return graph_code + + xoffset = 0 + yoffset = 0 + if not fgui_graph.pivot_is_anchor: + size = fgui_graph.size + xoffset = int(fgui_graph.pivot[0] * size[0]) + yoffset = int(fgui_graph.pivot[1] * size[1]) + + # 空白使用Null。 + if fgui_graph.type is None: + # graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}") + self.graph_template_dict['null'].replace('{image_name}', fgui_graph.id)\ + .replace('{width}', str(fgui_graph.size[0]))\ + .replace('{height}', str(fgui_graph.size[1])) + # 矩形(圆边矩形)。原生组件不支持圆边矩形,使用自定义shader实现。 + elif fgui_graph.type == "rect": + # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。 + graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\ + .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\ + .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ + .replace('{image_size}', str(fgui_graph.size))\ + .replace('{round_radius}', str(fgui_graph.corner_radius))\ + .replace('{stroke_thickness}', str(fgui_graph.stroke_width))\ + .replace('{xysize}', str(fgui_graph.size))\ + .replace('{pos}', str(fgui_graph.xypos))\ + .replace('{anchor}', str(fgui_graph.pivot))\ + .replace('{xoffset}', str(xoffset))\ + .replace('{yoffset}', str(yoffset))\ + .replace('{transform_anchor}', str(True)) \ + .replace('{rotate}', str(fgui_graph.rotation))\ + .replace('{alpha}', str(fgui_graph.alpha))\ + .replace('{xzoom}', str(fgui_graph.scale[0]))\ + .replace('{yzoom}', str(fgui_graph.scale[1])) + # 椭圆(圆形)。Ren'Py尚不支持椭圆,使用自定义shader实现。 + elif fgui_graph.type == "eclipse": + graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\ + .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\ + .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ + .replace('{image_size}', str(fgui_graph.size))\ + .replace('{stroke_thickness}', str(fgui_graph.stroke_width))\ + .replace('{xysize}', str(fgui_graph.size))\ + .replace('{pos}', str(fgui_graph.xypos))\ + .replace('{anchor}', str(fgui_graph.pivot))\ + .replace('{xoffset}', str(xoffset))\ + .replace('{yoffset}', str(yoffset))\ + .replace('{transform_anchor}', str(True)) \ + .replace('{rotate}', str(fgui_graph.rotation))\ + .replace('{alpha}', str(fgui_graph.alpha))\ + .replace('{xzoom}', str(fgui_graph.scale[0]))\ + .replace('{yzoom}', str(fgui_graph.scale[1])) + elif fgui_graph.type == "regular_polygon": + print("regular_polygon not implemented.") + pass + # 在整个脚本对象中添加graph定义。 + graph_code.append(graph_img_def) + + return graph_code + def generate_screen(self, component): """ 生成screen定义。目标样例: @@ -199,6 +282,7 @@ def generate_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}fixed:") self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"") self.indent_level_down() @@ -298,6 +382,76 @@ def generate_text_style(self, fgui_text, style_name): self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}") self.style_code.append("") + @staticmethod + def get_child_type(displayable): + if isinstance(displayable, FguiImage): + return DisplayableChildType.IMAGE + elif isinstance(displayable, FguiText): + return DisplayableChildType.TEXT + elif isinstance(displayable, FguiGraph): + return DisplayableChildType.GRAPH + elif isinstance(displayable, FguiComponent): + return DisplayableChildType.COMPONENT + else: + return DisplayableChildType.OTHER + + def generate_button_children(self, fgui_button): + """ + 生成按钮各状态的子组件。 + """ + + # 4种状态的子组件列表 + idle_child_list = [] + hover_child_list = [] + selected_child_list = [] + selected_hover_child_list = [] + # 非激活状体子组件列表 + insensitive_child_list = [] + # 始终显示的子组件列表 + always_show_child_list = [] + # 5种类型的子组件字典 + state_children_dict = { + 'idle': idle_child_list, + 'hover': hover_child_list, + 'selected': selected_child_list, + 'selected_hover': selected_hover_child_list, + 'insensitive': insensitive_child_list, + 'always_show': always_show_child_list + } + state_index_name_dict = { + 0: 'idle', + 1: 'hover', + 2: 'selected', + 3: 'selected_hover', + 4: 'insensitive', + None: 'always_show' + } + # 默认按钮控制器为button,并且必定有4种状态,顺序分别为idle、hover、selected、selected_hover。 + # 检查按钮控制器名称 + if fgui_button.controller_list[0].name != 'button': + print("按钮控制器名不是button。") + return state_children_dict + # 检查按钮控制器的状态列表 + state_list = fgui_button.controller_list[0].page_index_dict.keys() + state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态 + if state_number < 4: + print("按钮控制器状态总数小于4。") + return state_children_dict + + # 将displayable_list中的子组件按状态分别添加到对应列表中 + for displayable in fgui_button.display_list.displayable_list: + if displayable.gear_display is None: + state_children_dict['always_show'].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable))) + break + for i in range(0, state_number): + if displayable.gear_display and str(i) in displayable.gear_display.controller_index: + state_children_dict[state_index_name_dict[i]].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable))) + continue + # print(state_children_dict) + + return state_children_dict + + def generate_button_screen(self, component): """ @@ -330,33 +484,93 @@ def generate_button_screen(self, component): self.dismiss_action_list.clear() self.style_code.clear() + # 4种状态的子组件列表 + idle_child_list = [] + hover_child_list = [] + selected_child_list = [] + selected_hover_child_list = [] + # 非激活状体子组件列表 + insensitive_child_list = [] + # 始终显示的子组件列表 + always_show_child_list = [] + # 5种类型的子组件字典 + state_children_dict = { + 'idle': idle_child_list, + 'selected': selected_child_list, + 'hover': hover_child_list, + 'selected_hover': selected_hover_child_list, + 'insensitive': insensitive_child_list, + 'always_show': always_show_child_list + } + + state_index_name_dict = { + 0: 'idle', + 1: 'selected', + 2: 'hover', + 3: 'selected_hover', + 4: 'insensitive', + None: 'always_show' + } + + # 图片id与name映射关系 + image_id_name_mapping = {} + if component.extention != 'Button': print("组件类型不是按钮") return + # 默认按钮控制器为button,并且必定有4种状态,顺序分别为idle、hover、selected、selected_hover。 + # 可扩展为5种,第5种必需为insensitive,表示按钮不激活状态。 + if component.controller_list[0].name != 'button': + print("按钮控制器名不是button。") + return + # 检查按钮控制器的状态列表 + state_list = component.controller_list[0].page_index_dict.keys() + state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态 + if state_number < 4: + print("按钮控制器状态总数小于4。") + return + # 生成按钮组件screen self.screen_definition_head.append("# 按钮组件Screen") self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来") - # screen_code.append("") id = component.id button_name = component.name xysize = component.size background = self.default_background + default_title = '' + # state_children_dict = self.generate_button_children(component) for displayable in component.display_list.displayable_list: + # 不带显示控制器表示始终显示 + if displayable.gear_display is None: + # state_children_dict['always_show'].append((displayable, FguiToRenpyConverter.get_child_type(displayable))) + # 加入到所有状态显示列表中 + if displayable.name != 'title': + for state_list in state_children_dict.values(): + state_list.append((displayable, FguiToRenpyConverter.get_child_type(displayable))) + else: + pass + # 其他状态根据枚举值加入各列表 + else: + for i in range(0, state_number): + if str(i) in displayable.gear_display.controller_index: + state_children_dict[state_index_name_dict[i]].append((displayable, FguiToRenpyConverter.get_child_type(displayable))) + continue # 图片组件 if isinstance(displayable, FguiImage): for image in self.fgui_assets.package_desc.image_list: if displayable.src == image.id: - background = image.name + image_id_name_mapping[displayable.id] = image.name break - # TODO - # 处理不同图片分别用于idle、hover、selected_idle和selected_hover的情况 - # 文本组件 + # print(image_id_name_mapping) + elif isinstance(displayable, FguiGraph): + self.renpy_code.extend(self.generate_graph_definitions(displayable)) + # break + # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。 # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 - # if isinstance(displayable, FguiText) and displayable.name == 'title': - if isinstance(displayable, FguiText): + elif isinstance(displayable, FguiText) and displayable.name == 'title': # 重置缩进级别 self.reset_indent_level() default_title = displayable.text @@ -364,6 +578,19 @@ def generate_button_screen(self, component): self.generate_text_style(displayable, f"{button_name}_text") # self.screen_code.extend(self.style_code) + # 根据state_children_dict生成各种对应状态的background + # idle_background + self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle']) + # selected_background + self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected']) + # hover_background + self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'],) + # selected_hover_background + self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover']) + # insensitive_background + self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive']) + + # 重置缩进级别 self.reset_indent_level() self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):") @@ -377,7 +604,17 @@ def generate_button_screen(self, component): self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'") self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})") - self.screen_ui_code.append(f"{self.indent_str}background '{background}'") + # self.screen_ui_code.append(f"{self.indent_str}background '{background}'") + if state_children_dict['idle']: + self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'") + if state_children_dict['selected']: + self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'") + if state_children_dict['hover']: + self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'") + if state_children_dict['selected_hover']: + self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'") + if state_children_dict['insensitive']: + self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'") self.screen_ui_code.append(f"{self.indent_str}text title") self.screen_ui_code.append(f"{self.indent_str}action actions") # 一些按钮特性 @@ -397,6 +634,40 @@ def generate_button_screen(self, component): def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"): return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5) + def generate_image_object(self, image_name, displayable_list): + """ + 生成image对象,入参为一些子组件列表,自带transform。 + 暂时不考虑允许列表中的子组件添加额外transform,只单纯堆叠。 + """ + if len(displayable_list) <= 0: + return + image_definitions = [] + image_definitions.append("# 图像定义") + image_definitions.append("# 合成的图像对象") + indent_level = self.root_indent_level + self.reset_indent_level() + image_definitions.append(f"image {image_name}:") + self.indent_level_up() + for displayable, displayalbe_type in displayable_list: + image_definitions.append(f"{self.indent_str}contains:") + self.indent_level_up() + if displayalbe_type == DisplayableChildType.IMAGE: + name = self.fgui_assets.get_componentname_by_id(displayable.src) + image_definitions.append(f"{self.indent_str}'{name}'") + elif displayalbe_type == DisplayableChildType.GRAPH: + image_definitions.append(f"{self.indent_str}'{displayable.id}'") + elif displayalbe_type == DisplayableChildType.TEXT: + image_definitions.append(f"{self.indent_str}Text({displayable.text})") + # 其他类型暂时用空对象占位 + else: + image_definitions.append(f"{self.indent_str}Null()") + self.indent_level_down() + + image_definitions.append("") + + self.renpy_code.extend(image_definitions) + self.root_indent_level = indent_level + def generate_image_displayable(self, fgui_image): """ 生成图片组件。 @@ -459,54 +730,8 @@ def generate_graph_displayable(self, fgui_graph): print("It is not a graph displayable.") return graph_code - # 空白直接使用Null。后续可能存在一些relation相关不匹配。 - if fgui_graph.type is None: - graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}") - # 矩形(圆边矩形)。原生组件不支持圆边矩形,使用自定义shader实现。 - elif fgui_graph.type == "rect": - # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。 - graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\ - .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\ - .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ - .replace('{image_size}', str(fgui_graph.size))\ - .replace('{round_radius}', str(fgui_graph.corner_radius))\ - .replace('{stroke_thickness}', str(fgui_graph.stroke_width)) - # 直接在整个脚本对象中添加graph定义。 - self.renpy_code.append(graph_img_def) - # 椭圆(圆形)。Ren'Py尚不支持椭圆,使用自定义shader实现。 - elif fgui_graph.type == "eclipse": - graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\ - .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\ - .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ - .replace('{image_size}', str(fgui_graph.size))\ - .replace('{stroke_thickness}', str(fgui_graph.stroke_width)) - # 直接在整个脚本对象中添加graph定义。 - self.renpy_code.append(graph_img_def) - - # 生成screen中的部分,带transform。 - graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\":") - self.indent_level_up() - graph_code.append(f"{self.indent_str}xysize {fgui_graph.size}") - graph_code.append(f"{self.indent_str}pos {fgui_graph.xypos}") - graph_code.append(f"{self.indent_str}at transform:") - self.indent_level_up() - graph_code.append(f"{self.indent_str}anchor {fgui_graph.pivot}") - if not fgui_graph.pivot_is_anchor: - size = fgui_graph.size - xoffset = int(fgui_graph.pivot[0] * size[0]) - yoffset = int(fgui_graph.pivot[1] * size[1]) - graph_code.append(f"{self.indent_str}xoffset {xoffset}") - graph_code.append(f"{self.indent_str}yoffset {yoffset}") - graph_code.append(f"{self.indent_str}transform_anchor True") - if fgui_graph.rotation: - graph_code.append(f"{self.indent_str}rotate {fgui_graph.rotation}") - if fgui_graph.alpha != 1.0: - graph_code.append(f"{self.indent_str}alpha {fgui_graph.alpha}") - if fgui_graph.scale != (1.0, 1.0): - graph_code.append(f"{self.indent_str}xzoom {fgui_graph.scale[0]} yzoom {fgui_graph.scale[1]}") - self.indent_level_down() - - self.indent_level_down() + self.renpy_code.extend(self.generate_graph_definitions(fgui_graph)) + graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\"") return graph_code diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt index 1f77827..cd6a884 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt @@ -1,2 +1,13 @@ # 抗锯齿的椭圆(圆形)图像定义 -image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01) +image {image_name}: + Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01) + xysize {xysize} + pos {pos} + anchor {anchor} + xoffset {xoffset} + yoffset {yoffset} + transform_anchor {transform_anchor} + rotate {rotate} + alpha {alpha} + xzoom {xzoom} + yzoom {yzoom} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt index 8a36cb8..364e171 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt @@ -1,2 +1,13 @@ # 椭圆(圆形)图像定义 -image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}) +image {image_name}: + Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}) + xysize {xysize} + pos {pos} + anchor {anchor} + xoffset {xoffset} + yoffset {yoffset} + transform_anchor {transform_anchor} + rotate {rotate} + alpha {alpha} + xzoom {xzoom} + yzoom {yzoom} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt new file mode 100644 index 0000000..4bc008d --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt @@ -0,0 +1,2 @@ +image {image_name}: + Null(width={width}, height={height}) diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt index e54000c..75237be 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt @@ -1,2 +1,13 @@ # 带光滑渐变的圆角矩形图像定义 -image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0) +image {image_name}: + Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0) + xysize {xysize} + pos {pos} + anchor {anchor} + xoffset {xoffset} + yoffset {yoffset} + transform_anchor {transform_anchor} + rotate {rotate} + alpha {alpha} + xzoom {xzoom} + yzoom {yzoom} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt index 9dab793..eb60024 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt @@ -1,2 +1,13 @@ # 圆角矩形图像定义 -image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}) +image {image_name}: + Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}) + xysize {xysize} + pos {pos} + anchor {anchor} + xoffset {xoffset} + yoffset {yoffset} + transform_anchor {transform_anchor} + rotate {rotate} + alpha {alpha} + xzoom {xzoom} + yzoom {yzoom} From ac12c0e87f4c9142c8adf73e7a50aa42838dd775 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Tue, 4 Nov 2025 23:42:53 +0800 Subject: [PATCH 07/19] Add text style within Button 1. No "title" text within Buttons, add styles. 2. Fixed some problems of FguiAssetsParseLib Module. --- src/fgui_converter/FguiAssetsParseLib.py | 5 +- .../utils/renpy/Fgui2RenpyConverter.py | 78 ++++++++++++++----- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index 9d28fbb..34e18cb 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -750,9 +750,9 @@ def __init__(self, display_item_tree, package_description_id=None): self.item_url = None if package_description_id: self.package_description_id = package_description_id - get_item_id(package_description_id) + self.get_item_id(package_description_id) - def get_item_id(packageDescription_id): + def get_item_id(self, packageDescription_id): self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):] class FguiGearBase: @@ -868,6 +868,7 @@ def __init__(self, gear_item_tree): raise ValueError("xml tag is not gearSize.") super().__init__(gear_item_tree) self.index_value_dict = {} # 该字典存放控制器索引与对应尺寸 + index_values = self.values if self.values: for i in range(len(self.values)): item = index_values[i].split(",") diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 72024c0..dfa6605 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -284,7 +284,7 @@ def generate_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) - self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"") + self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{displayable.id}'") self.indent_level_down() self.screen_code.extend(self.screen_definition_head) @@ -348,9 +348,9 @@ def generate_text_style(self, fgui_text, style_name): # 需将字体名添加到font_name_list中,并替换renpy_font_map_definition模板中对应内容。 if not text_font in self.font_name_list: self.font_name_list.append(text_font) - self.style_code.append(f"{style_indent}font \"{text_font}\"") + self.style_code.append(f"{style_indent}font '{text_font}'") self.style_code.append(f"{style_indent}size {fgui_text.font_size}") - self.style_code.append(f"{style_indent}color \"{fgui_text.text_color}\"") + self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'") xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) self.style_code.append(f"{style_indent}align ({xalign}, {yalign})") # Ren'Py中使用两层outlines分别实现投影和描边 @@ -360,9 +360,9 @@ def generate_text_style(self, fgui_text, style_name): shadow_width = fgui_text.stroke_size has_outline = fgui_text.stroke_color if has_shadow: - shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" if has_shadow and not has_outline: self.style_code.append(f"{style_indent}outlines [{shadow_outline}]") if not has_shadow and has_outline: @@ -541,7 +541,7 @@ def generate_button_screen(self, component): background = self.default_background default_title = '' - # state_children_dict = self.generate_button_children(component) + for displayable in component.display_list.displayable_list: # 不带显示控制器表示始终显示 if displayable.gear_display is None: @@ -657,7 +657,9 @@ def generate_image_object(self, image_name, displayable_list): elif displayalbe_type == DisplayableChildType.GRAPH: image_definitions.append(f"{self.indent_str}'{displayable.id}'") elif displayalbe_type == DisplayableChildType.TEXT: - image_definitions.append(f"{self.indent_str}Text({displayable.text})") + # image_definitions.append(f"{self.indent_str}Text({displayable.text})") + text_displayable_string = self.generate_text_displayable_string(displayable) + image_definitions.append(f"{self.indent_str}{text_displayable_string}") # 其他类型暂时用空对象占位 else: image_definitions.append(f"{self.indent_str}Null()") @@ -668,6 +670,44 @@ def generate_image_object(self, image_name, displayable_list): self.renpy_code.extend(image_definitions) self.root_indent_level = indent_level + def generate_text_displayable_string(self, fgui_text): + text_displayable_string = '' + if not isinstance(fgui_text, FguiText): + print("It is not a text displayable.") + return text_displayable_string + text_anchor_param = f"anchor={fgui_text.pivot}" + text_transformanchor = f"transform_anchor=True" + text_pos_param = f"pos={fgui_text.xypos}" + text_size_param = f"xysize={fgui_text.size}" + text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" + if not text_font in self.font_name_list: + self.font_name_list.append(text_font) + text_font_param = f"font='{text_font}'" + text_font_size_param = f"size={fgui_text.font_size}" + text_font_color_param = f"color='{fgui_text.text_color}'" + text_textalign_param = f"textalign=0.5" + text_bold_param = f"bold={fgui_text.bold}" + text_italic_param = f"italic={fgui_text.italic}" + text_underline_param = f"underline={fgui_text.underline}" + text_strike_param = f"strike={fgui_text.strike}" + has_shadow = fgui_text.shadow_color + shadow_width = fgui_text.stroke_size + has_outline = fgui_text.stroke_color + text_outlines_parame = 'outlines=[]' + if has_shadow: + shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + if has_outline: + stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + if has_shadow and not has_outline: + text_outlines_parame = f"outlines=[{shadow_outline}]" + if not has_shadow and has_outline: + text_outlines_parame = f"outlines=[{stroke_outline}]" + if has_shadow and has_outline: + text_outlines_parame = f"outlines=[{shadow_outline}, {stroke_outline}]" + + text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" + return text_displayable_string + def generate_image_displayable(self, fgui_image): """ 生成图片组件。 @@ -681,7 +721,7 @@ def generate_image_displayable(self, fgui_image): for image in self.fgui_assets.package_desc.image_list: if fgui_image.src == image.id: image_name = image.name - image_code.append(f"{self.indent_str}add \"{image_name}\":") + image_code.append(f"{self.indent_str}add '{image_name}':") self.indent_level_up() image_code.append(f"{self.indent_str}pos {fgui_image.xypos}") # 必须指定,旋转和缩放都需要使用。 @@ -731,7 +771,7 @@ def generate_graph_displayable(self, fgui_graph): return graph_code self.renpy_code.extend(self.generate_graph_definitions(fgui_graph)) - graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\"") + graph_code.append(f"{self.indent_str}add '{fgui_graph.id}'") return graph_code @@ -770,7 +810,7 @@ def generate_text_displayable(self, fgui_text): self.indent_level_down() else: - text_code.append(f"{self.indent_str}text \"{text_str}\":") + text_code.append(f"{self.indent_str}text '{text_str}':") self.indent_level_up() text_code.append(f"{self.indent_str}xysize {fgui_text.size}") text_code.append(f"{self.indent_str}pos {fgui_text.xypos}") @@ -799,22 +839,22 @@ def generate_text_displayable(self, fgui_text): prompt_str = fgui_text.prompt.replace("\n", "\\n").replace("\r", "\\n") text_code.append(f"{self.indent_str}showif check_input_length({fgui_text.name}_input_value):") self.indent_level_up() - text_code.append(f"{self.indent_str}text \"{prompt_str}\":") + text_code.append(f"{self.indent_str}text '{prompt_str}':") self.indent_level_up() # 暂时使用与input相同样式 text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" if not text_font in self.font_name_list: self.font_name_list.append(text_font) - text_code.append(f"{self.indent_str}font \"{text_font}\"") + text_code.append(f"{self.indent_str}font '{text_font}'") text_code.append(f"{self.indent_str}size {fgui_text.font_size}") - text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"") + text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'") has_shadow = fgui_text.shadow_color shadow_width = fgui_text.stroke_size has_outline = fgui_text.stroke_color if has_shadow: - shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" if has_shadow and not has_outline: text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") if not has_shadow and has_outline: @@ -848,9 +888,9 @@ def generate_text_displayable(self, fgui_text): # 需将字体名添加到font_name_list中,并替换renpy_font_map_definition模板中对应内容。 if not text_font in self.font_name_list: self.font_name_list.append(text_font) - text_code.append(f"{self.indent_str}font \"{text_font}\"") + text_code.append(f"{self.indent_str}font '{text_font}'") text_code.append(f"{self.indent_str}size {fgui_text.font_size}") - text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"") + text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'") # Ren'Py中使用两层outlines分别实现投影和描边 # FGUI中的投影包含描边,投影的size等于描边的size,默认值为0;Ren'Py中允许outlines为0,依然有效果。 # Ren'Py中的property outlines是列表,先投影后描边。 @@ -858,9 +898,9 @@ def generate_text_displayable(self, fgui_text): shadow_width = fgui_text.stroke_size has_outline = fgui_text.stroke_color if has_shadow: - shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" if has_shadow and not has_outline: text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") if not has_shadow and has_outline: From 8076bc9db65ea5dd30eea791b928b6edb481a8f6 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Wed, 5 Nov 2025 15:17:25 +0800 Subject: [PATCH 08/19] Update Text drop-shadow outline method Add a extra outline to keep consistent with FGUI Text Component view. --- .../utils/renpy/Fgui2RenpyConverter.py | 97 +++++++------------ 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index dfa6605..d3391a2 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -353,22 +353,10 @@ def generate_text_style(self, fgui_text, style_name): self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'") xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) self.style_code.append(f"{style_indent}align ({xalign}, {yalign})") - # Ren'Py中使用两层outlines分别实现投影和描边 - # FGUI中的投影包含描边,投影的size等于描边的size,默认值为0;Ren'Py中允许outlines为0,依然有效果 - # Ren'Py中的property outlines可以是列表,先投影后描边 - has_shadow = fgui_text.shadow_color - shadow_width = fgui_text.stroke_size - has_outline = fgui_text.stroke_color - if has_shadow: - shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" - if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" - if has_shadow and not has_outline: - self.style_code.append(f"{style_indent}outlines [{shadow_outline}]") - if not has_shadow and has_outline: - self.style_code.append(f"{style_indent}outlines [{stroke_outline}]") - if has_shadow and has_outline: - self.style_code.append(f"{style_indent}outlines [{shadow_outline}, {stroke_outline}]") + + text_outline_string = self.generate_text_outline_string(fgui_text) + self.style_code.append(f"{style_indent}outlines {text_outline_string}") + # 默认两侧居中对齐 self.style_code.append(f"{style_indent}textalign 0.5") # 粗体、斜体、下划线、删除线 @@ -642,8 +630,8 @@ def generate_image_object(self, image_name, displayable_list): if len(displayable_list) <= 0: return image_definitions = [] - image_definitions.append("# 图像定义") - image_definitions.append("# 合成的图像对象") + image_definitions.append("# image对象定义") + image_definitions.append("# 使用其他image对象的组合") indent_level = self.root_indent_level self.reset_indent_level() image_definitions.append(f"image {image_name}:") @@ -670,6 +658,26 @@ def generate_image_object(self, image_name, displayable_list): self.renpy_code.extend(image_definitions) self.root_indent_level = indent_level + @staticmethod + def generate_text_outline_string(fgui_text): + outline_string = '[]' + has_shadow = fgui_text.shadow_color + has_outline = fgui_text.stroke_color + if has_shadow: + shadow_outline = f"(absolute(0), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + if has_outline: + stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" + if has_shadow and not has_outline: + outline_string = f"[{shadow_outline}]" + if not has_shadow and has_outline: + outline_string = f"[{stroke_outline}]" + # Ren'Py中的投影不包含其他描边。为保持与FGUI效果一直,需要添加一层,宽度与描边一致,颜色、偏移与投影一致。 + if has_shadow and has_outline: + extra_shadow = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" + outline_string = f"[{shadow_outline}, {extra_shadow}, {stroke_outline}]" + return outline_string + + def generate_text_displayable_string(self, fgui_text): text_displayable_string = '' if not isinstance(fgui_text, FguiText): @@ -690,20 +698,9 @@ def generate_text_displayable_string(self, fgui_text): text_italic_param = f"italic={fgui_text.italic}" text_underline_param = f"underline={fgui_text.underline}" text_strike_param = f"strike={fgui_text.strike}" - has_shadow = fgui_text.shadow_color - shadow_width = fgui_text.stroke_size - has_outline = fgui_text.stroke_color - text_outlines_parame = 'outlines=[]' - if has_shadow: - shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" - if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))" - if has_shadow and not has_outline: - text_outlines_parame = f"outlines=[{shadow_outline}]" - if not has_shadow and has_outline: - text_outlines_parame = f"outlines=[{stroke_outline}]" - if has_shadow and has_outline: - text_outlines_parame = f"outlines=[{shadow_outline}, {stroke_outline}]" + + text_outline_string = self.generate_text_outline_string(fgui_text) + text_outlines_parame = 'outlines={text_outline_string}' text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" return text_displayable_string @@ -848,19 +845,9 @@ def generate_text_displayable(self, fgui_text): text_code.append(f"{self.indent_str}font '{text_font}'") text_code.append(f"{self.indent_str}size {fgui_text.font_size}") text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'") - has_shadow = fgui_text.shadow_color - shadow_width = fgui_text.stroke_size - has_outline = fgui_text.stroke_color - if has_shadow: - shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" - if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" - if has_shadow and not has_outline: - text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") - if not has_shadow and has_outline: - text_code.append(f"{self.indent_str}outlines [{stroke_outline}]") - if has_shadow and has_outline: - text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]") + text_outline_string = self.generate_text_outline_string(fgui_text) + text_code.append(f"{self.indent_str}outlines {text_outline_string}") + if fgui_text.letter_spacing: text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}") if fgui_text.leading: @@ -891,22 +878,10 @@ def generate_text_displayable(self, fgui_text): text_code.append(f"{self.indent_str}font '{text_font}'") text_code.append(f"{self.indent_str}size {fgui_text.font_size}") text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'") - # Ren'Py中使用两层outlines分别实现投影和描边 - # FGUI中的投影包含描边,投影的size等于描边的size,默认值为0;Ren'Py中允许outlines为0,依然有效果。 - # Ren'Py中的property outlines是列表,先投影后描边。 - has_shadow = fgui_text.shadow_color - shadow_width = fgui_text.stroke_size - has_outline = fgui_text.stroke_color - if has_shadow: - shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))" - if has_outline: - stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))" - if has_shadow and not has_outline: - text_code.append(f"{self.indent_str}outlines [{shadow_outline}]") - if not has_shadow and has_outline: - text_code.append(f"{self.indent_str}outlines [{stroke_outline}]") - if has_shadow and has_outline: - text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]") + + text_outline_string = self.generate_text_outline_string(fgui_text) + text_code.append(f"{self.indent_str}outlines {text_outline_string}") + # 字间距与行距 if fgui_text.letter_spacing: text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}") From f61503017a9bd5bacbc9c8421e787d76b52fbac8 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Fri, 7 Nov 2025 23:56:14 +0800 Subject: [PATCH 09/19] Support "Slider" component 1. Add "Slider" converting. 2. Graph name add component name as prefix, because of same graph id within different components. 3. Support color hex string of 6 characters(rrggbb), with default alpha value(ff). 4. Add simple slider property and "FguiRelation" class. --- src/fgui_converter/FguiAssetsParseLib.py | 68 +++- .../utils/renpy/Fgui2RenpyConverter.py | 317 ++++++++++++++---- 2 files changed, 316 insertions(+), 69 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index 34e18cb..7be9ef2 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from re import M import sys import os from lxml import etree @@ -252,7 +253,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None): self.id = id self.name = name self.package_desc_id = package_desc_id - self.size = component_etree.get("size") + size = component_etree.get("size") + self.size = tuple(map(int, size.split(","))) if size else (0,0) self.extention = component_etree.get("extention") self.mask = component_etree.get("mask") # 轴心。默认值为(0.0, 0.0)。 @@ -329,7 +331,7 @@ class FguiProgressBar(FguiComponent): """ def __init__(self, component_etree, id, name, package_desc_id=None): super().__init__(component_etree, id, name, package_desc_id=None) - combobox = component_etree.find("ProgressBar") + self.progressbar = component_etree.find("ProgressBar") class FguiSlider(FguiComponent): """ @@ -339,7 +341,8 @@ class FguiSlider(FguiComponent): """ def __init__(self, component_etree, id, name, package_desc_id=None): super().__init__(component_etree, id, name, package_desc_id=None) - combobox = component_etree.find("Slider") + self.slider = component_etree.find("Slider") + self.title_type = self.slider.get("titleType") class FguiWindow(FguiComponent): """ @@ -370,7 +373,7 @@ def __init__(self, display_list_etree, package_desc_id=None): # print(f"package_desc_id: {package_desc_id}") self.displayable_list = [] for displayable in self.display_list_etree: - print(displayable.tag) + # print(displayable.tag) # 根据标签类型创建相应的FguiDisplayable对象 if displayable.tag == "graph": self.displayable_list.append(FguiGraph(displayable)) @@ -388,14 +391,19 @@ def __init__(self, display_list_etree, package_desc_id=None): def hex_aarrggbb_to_rgba(hex_color): """ - 将一个 8 位的十六进制颜色字符串 (AARRGGBB) 转换为一个 RGBA 元组。 + 将一个8位的十六进制颜色字符串(AARRGGBB)或6位的十六进制颜色字符串(RRGGBB)转换为一个 RGBA 元组。 """ # 移除字符串头部的 '#' - clean_hex = hex_color.lstrip('#') + clean_hex = hex_color.lstrip('#').lower() + hex_str_len = len(clean_hex) - # 检查处理后的字符串长度是否为 8 - if len(clean_hex) != 8: - raise ValueError("输入的十六进制字符串必须是8位 (AARRGGBB)") + # 检查处理后的字符串长度是否为8或6 + if hex_str_len != 8 and hex_str_len != 6: + raise ValueError("输入的十六进制字符串必须是8位(AARRGGBB)或6位(RRGGBB)") + + # 6位字符则加上alpha通道的默认值 ff + if hex_str_len ==6: + clean_hex = 'ff' + clean_hex # 按 AARRGGBB 的顺序提取,并从16进制转换为10进制整数 try: @@ -460,7 +468,7 @@ def __init__(self, display_item_tree, package_description_id=None): self.xypos = tuple(map(int, xy.split(","))) # 尺寸。若为None:image的size默认与image对象一致。 size = self.display_item_tree.get("size") - self.size = tuple(map(int, size.split(","))) if size else None + self.size = tuple(map(int, size.split(","))) if size else (0,0) # 保持比例。若为None,则将宽和高分别缩放到size。 self.aspect = (self.display_item_tree.get("aspect") == "true") # 缩放。默认值为(1.0, 1.0)。 @@ -502,6 +510,9 @@ def __init__(self, display_item_tree, package_description_id=None): # Button,按钮专有属性 self.button_property = None + # Slider,滑动条专有属性 + self.slider_property = None + # gear属性 self.gear_display = None self.gear_display_2 = None @@ -512,9 +523,12 @@ def __init__(self, display_item_tree, package_description_id=None): self.gear_text = None self.gear_icon = None + # relation + self.relations = None + # 一级子对象 self.child_num = len(self.display_item_tree) - # 控制器gear子组件 + # 控制器gear子组件和relation关联项 for i in range(self.child_num): if self.display_item_tree[i].tag == "gearDisplay" : self.gear_display = FguiGearDisplay(self.display_item_tree[i]) @@ -534,6 +548,10 @@ def __init__(self, display_item_tree, package_description_id=None): self.gear_icon = FguiGearIcon(self.display_item_tree[i]) elif self.display_item_tree[i].tag == "Button" : self.button_property = FguiButtonProperty(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "relation" : + self.relations = FguiRelation(self.display_item_tree[i]) + elif self.display_item_tree[i].tag == "Slider" : + self.slider_property = FguiSliderProperty(self.display_item_tree[i]) else: print(f"Tag not parse: {self.display_item_tree[i].tag}.") @@ -755,6 +773,34 @@ def __init__(self, display_item_tree, package_description_id=None): def get_item_id(self, packageDescription_id): self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):] +class FguiRelation: + """ + 组件关联属性对象。表示与其他组件的相对关系。 + 通常是一个target:“关联对象”-sidePair“关联方式”的类字典结构。 + """ + def __init__(self, relation_item_tree): + if relation_item_tree.tag != "relation": + raise ValueError("xml tag is not relation.") + self.relation_item_tree = relation_item_tree + self.relation_dict = {} + key = self.relation_item_tree.get("target") + value = self.relation_item_tree.get("sidePair") + self.relation_dict[key] = value + # print(self.relation_dict) + +class FguiSliderProperty: + """ + 滑块的数值属性。分别包含最小值、最大值与当前值。 + 如果某一项属性未出现则等于默认值0。 + """ + def __init__(self, slider_property_tree): + if slider_property_tree.tag != "Slider": + raise ValueError("xml tag is not Slider.") + self.slider_property_tree = slider_property_tree + self.current_value = self.slider_property_tree.get("value", 0) + self.min_value = self.slider_property_tree.get("min", 0) + self.max_value = self.slider_property_tree.get("max", 0) + class FguiGearBase: """ Displayable控制器设置相关的基类。 diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index d3391a2..f64a425 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import dis import sys import os import re @@ -8,7 +9,9 @@ import shutil from enum import IntEnum +from tkinter import HORIZONTAL, NO, VERTICAL +# 复合型组件的子组件枚举类型 class DisplayableChildType(IntEnum): NULL = 0 IMAGE = 1 @@ -17,6 +20,11 @@ class DisplayableChildType(IntEnum): COMPONENT = 4 OTHER = 5 +# bar、scrollbar、slider等组件的方向枚举类型 +class BarOrientationType(IntEnum): + HORIZONTAL = 0 + VERTICAL = 1 + # 添加当前目录到Python路径,以便导入FguiAssetsParseLib sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -28,6 +36,23 @@ class FguiToRenpyConverter: 将FairyGUI资源转换为Ren'Py的screen语言 """ + # 静态变量 + # 文本对齐的转义字典 + align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0} + # 4个空格作为缩进基本单位 + indent_unit = ' ' + + # 默认背景色,在某些未指定image的情况下用作填充。 + default_background = '#fff' + + # 菜单类界面名称,需要添加 tag menu。 + menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences'] + + # 模态类界面名称,需要添加 Modal True。 + modal_screen_name_list = ['confirm'] + + + def __init__(self, fgui_assets): self.fgui_assets = fgui_assets self.renpy_code = [] @@ -42,22 +67,14 @@ def __init__(self, fgui_assets): self.screen_has_dismiss = False self.dismiss_action_list = [] - # 文本对齐的转义字典 - self.align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0} - # 字体名称列表。SourceHanSansLite为Ren'Py默认字体。 self.font_name_list = ["SourceHanSansLite"] - # 4个空格作为缩进基本单位 - self.indent_unit = ' ' # 组件缩进级别 self.root_indent_level = 0 # 缩进字符串 self.indent_str = '' - # 默认背景色,在某些未指定image的情况下用作填充。 - self.default_background = '#fff' - # 部分模板与预设目录 # self.renpy_template_dir = 'renpy_templates' self.renpy_template_dir = os.environ.get('RENPY_TEMPLATES_DIR', @@ -140,9 +157,10 @@ def generate_image_definitions(self): image_definitions.append("") self.renpy_code.extend(image_definitions) - def generate_graph_definitions(self, fgui_graph): + def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : str) -> list: """ - 生成图形组件定义。图形组件有多种类别: + 生成图形组件定义。返回字符串用于非screen定义。 + 图形组件有多种类别: None: 空白 rect: 矩形(可带圆角) eclipse: 椭圆(包括圆形) @@ -166,13 +184,13 @@ def generate_graph_definitions(self, fgui_graph): # 空白使用Null。 if fgui_graph.type is None: # graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}") - self.graph_template_dict['null'].replace('{image_name}', fgui_graph.id)\ + self.graph_template_dict['null'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\ .replace('{width}', str(fgui_graph.size[0]))\ .replace('{height}', str(fgui_graph.size[1])) # 矩形(圆边矩形)。原生组件不支持圆边矩形,使用自定义shader实现。 elif fgui_graph.type == "rect": # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。 - graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\ + graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\ .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\ .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ .replace('{image_size}', str(fgui_graph.size))\ @@ -190,7 +208,7 @@ def generate_graph_definitions(self, fgui_graph): .replace('{yzoom}', str(fgui_graph.scale[1])) # 椭圆(圆形)。Ren'Py尚不支持椭圆,使用自定义shader实现。 elif fgui_graph.type == "eclipse": - graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\ + graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\ .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\ .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\ .replace('{image_size}', str(fgui_graph.size))\ @@ -213,6 +231,148 @@ def generate_graph_definitions(self, fgui_graph): return graph_code + def generate_slider_style(self, fgui_slider : FguiSlider): + """ + 生成滑块样式。 + Ren'Py生成的滑动条效果与FGUI略有不同。 + FGUI的左侧激活状态bar会在水平方向随滑块缩放,Ren'Py不会缩放。 + 目标样例: + image horizontal_idle_thumb_image: + "horizontal_thumb_base" + + image horizontal_hover_thumb_image: + "horizontal_thumb_active" + + image horizontal_idle_bar_image: + "horizontal_bar_base" + + image horizontal_hover_bar_image: + "horizontal_bar_active" + + style horizontal_slider: + bar_vertical False + xsize 540 + ysize 3 + thumb_offset 24 + left_bar "horizontal_hover_bar_image" + right_bar "horizontal_idle_bar_image" + thumb Fixed(Frame("horizontal_[prefix_]thumb_image",xsize=48,ysize=54,ypos=-22)) + """ + bar_image_definition_code = [] + style_definition_code = [] + slider_style_code = [] + if not isinstance(fgui_slider, FguiSlider): + print("It is not a slider.") + return slider_style_code + + # 默认为水平滑块。实际需要根据grip中relation的sidePair属性来确定。 + slider_type = BarOrientationType.HORIZONTAL + # bar的第一段,水平方向为right_bar,垂直方向为top_bar + first_bar_name = '' + # bar的第二段,水平方向为left_bar,垂直方向为bottom_bar + second_bar_name = '' + # 滑块图片名 + thumb_idle_name = '' + thumb_hover_name = '' + + # bar id,在grip的relation_dict中作为key查找值 + bar_id = '' + grip_com = None + # 滑块位置,用做偏移 + thumb_xpos, thumb_ypos = 0, 0 + + # slider 组件固定由两张图片(图形)和一个按钮构成,可能还有一个文本组件。 + # 图片(图形)固定名称为n0和bar,按钮固定名称grip,文本组件固定名称title。 + # 其他组件暂不处理。 + for displayable in fgui_slider.display_list.displayable_list: + # FGUI中滑动条的背景 + if displayable.name == 'n0': + if isinstance(displayable, FguiImage): + second_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src) + elif isinstance(displayable, FguiGraph): + bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_slider.name)) + second_bar_name = f"{fgui_slider.name}_{displayable.id}" + else: + print("Slider base is neither image nor graph.") + return slider_style_code + # FGUI中滑动条的可变bar部分 + if displayable.name == 'bar': + if isinstance(displayable, FguiImage): + first_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src) + elif isinstance(displayable, FguiGraph): + bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_slider.name)) + first_bar_name = f"{fgui_slider.name}_{displayable.id}" + else: + print("Slider bar is neither image nor graph.") + return slider_style_code + bar_id = displayable.id + # FGUI中滑动条的标题类型文本 + if displayable.name == 'title': + if isinstance(displayable, FguiText): + style_name = f"slider_{fgui_slider.id}_title" + self.generate_text_style(displayable, style_name) + else: + print("Slider title is not text.") + return slider_style_code + # FGUI中滑动条的滑块按钮 + if displayable.name == 'grip': + grip_com = self.fgui_assets.get_component_by_id(displayable.src) + if isinstance(grip_com, FguiButton): + # grip是按钮,会生成对应的image对象 + thumb_idle_name = f"{fgui_slider.name}_grip_idle_background" + thumb_hover_name = f"{fgui_slider.name}_grip_hover_background" + thumb_xpos, thumb_ypos = displayable.xypos + # 根据grip中relation的sidePair属性来确定方向 + side_pair_str = displayable.relations.relation_dict[bar_id] + # 只有该值表示垂直滑动条 + if side_pair_str == "bottom-bottom": + slider_type = BarOrientationType.VERTICAL + else: + print("Slider grp is not button.") + return slider_style_code + + # 生成bar和thumb的image + bar_image_definition_code.append(f"image {fgui_slider.name}_base_bar_image:") + bar_image_definition_code.append(f"{self.indent_unit}'{second_bar_name}'") + bar_image_definition_code.append(f"image {fgui_slider.name}_active_bar_image:") + bar_image_definition_code.append(f"{self.indent_unit}'{first_bar_name}'") + bar_image_definition_code.append(f"image {fgui_slider.name}_idle_thumb_image:") + bar_image_definition_code.append(f"{self.indent_unit}'{thumb_idle_name}'") + bar_image_definition_code.append(f"image {fgui_slider.name}_hover_thumb_image:") + bar_image_definition_code.append(f"{self.indent_unit}'{thumb_hover_name}'") + bar_image_definition_code.append("") + + is_vertical = (slider_type==BarOrientationType.VERTICAL) + thumb_offset = int(grip_com.size[1]/2) if is_vertical else int(grip_com.size[0]/2) + style_definition_code.append(f"style {fgui_slider.name}:") + style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}") + style_definition_code.append(f"{self.indent_unit}xysize {fgui_slider.size}") + style_definition_code.append(f"{self.indent_unit}thumb_offset {thumb_offset}") + if is_vertical: + style_definition_code.append(f"{self.indent_unit}top_bar '{second_bar_name}'") + style_definition_code.append(f"{self.indent_unit}bottom_bar '{first_bar_name}'") + thumb_ypos = 0 + else: + style_definition_code.append(f"{self.indent_unit}left_bar '{first_bar_name}'") + style_definition_code.append(f"{self.indent_unit}right_bar '{second_bar_name}'") + thumb_xpos = 0 + style_definition_code.append(f"{self.indent_unit}thumb Fixed(Frame('{fgui_slider.name}_[prefix_]thumb_image',xysize={grip_com.size},pos=({thumb_xpos},{thumb_ypos})))") + style_definition_code.append("") + + slider_style_code.extend(bar_image_definition_code) + slider_style_code.extend(style_definition_code) + + self.style_code.extend(slider_style_code) + + + @staticmethod + def is_menu_screen(screen_name): + return screen_name in FguiToRenpyConverter.menu_screen_name_list + + @staticmethod + def is_modal_screen(screen_name): + return screen_name in FguiToRenpyConverter.modal_screen_name_list + def generate_screen(self, component): """ 生成screen定义。目标样例: @@ -254,16 +414,27 @@ def generate_screen(self, component): id = component.id screen_name = component.name + # 界面入参列表 + screen_params = '' + # confirm 界面固定入参 + if screen_name == 'confirm': + screen_params = 'message, yes_action, no_action' + self.reset_indent_level() - self.screen_definition_head.append(f"screen {screen_name}():") + self.screen_definition_head.append(f"screen {screen_name}({screen_params}):") self.indent_level_up() + if self.is_menu_screen(screen_name): + self.screen_ui_code.append(f"{self.indent_str}tag menu\n") + if self.is_modal_screen(screen_name): + self.screen_ui_code.append(f"{self.indent_str}modal True") + self.screen_ui_code.append(f"{self.indent_str}zorder 200\n") for displayable in component.display_list.displayable_list: # 图片组件 if isinstance(displayable, FguiImage): self.screen_ui_code.extend(self.generate_image_displayable(displayable)) # 图形组件 elif isinstance(displayable, FguiGraph): - self.screen_ui_code.extend(self.generate_graph_displayable(displayable)) + self.screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name)) # 文本组件 elif isinstance(displayable, FguiText): self.screen_ui_code.extend(self.generate_text_displayable(displayable)) @@ -283,15 +454,20 @@ def generate_screen(self, component): self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 - parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) - self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{displayable.id}'") + if displayable.button_property: + parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) + else: + parameter_str = self.generate_button_parameter(None, displayable.custom_data) + self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'") self.indent_level_down() self.screen_code.extend(self.screen_definition_head) - self.screen_code.extend(self.screen_variable_code) - self.screen_code.append("") - self.screen_code.extend(self.screen_function_code) - self.screen_code.append("") + if self.screen_variable_code: + self.screen_code.extend(self.screen_variable_code) + self.screen_code.append("") + if self.screen_function_code: + self.screen_code.extend(self.screen_function_code) + self.screen_code.append("") # 添加只有1个生效的dismiss if self.screen_has_dismiss: self.screen_ui_code.append(f"{self.indent_str}dismiss:") @@ -299,9 +475,12 @@ def generate_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}modal False") dismiss_action_list = ', '.join(self.dismiss_action_list) self.screen_ui_code.append(f"{self.indent_str}action [{dismiss_action_list}]") - + + self.screen_ui_code.append("") self.screen_code.extend(self.screen_ui_code) + + def generate_button_parameter(self, button_title=None, original_actions_str=None): parameter_str = "" title_str = "" @@ -317,7 +496,7 @@ def generate_button_parameter(self, button_title=None, original_actions_str=None return parameter_str - def generate_text_style(self, fgui_text, style_name): + def generate_text_style(self, fgui_text : FguiText, style_name : str): """ 生成文本样式,专用于按钮标题,因为按钮中通常只有一个文本组件。 目标样例: @@ -331,14 +510,14 @@ def generate_text_style(self, fgui_text, style_name): textalign 0.5 """ - self.style_code.clear() + # self.style_code.clear() # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 if not isinstance(fgui_text, FguiText): print("It is not a text displayable.") return # 样式具有固定一档的缩进 style_indent = " " - default_title = fgui_text.text + # default_title = fgui_text.text # 定义样式 self.style_code.append(f"style {style_name}:") self.style_code.append(f"{style_indent}xysize {fgui_text.size}") @@ -357,8 +536,12 @@ def generate_text_style(self, fgui_text, style_name): text_outline_string = self.generate_text_outline_string(fgui_text) self.style_code.append(f"{style_indent}outlines {text_outline_string}") + + #设置最小宽度才能使text_align生效 + self.style_code.append(f"{style_indent}min_width {fgui_text.size[0]}") + xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) # 默认两侧居中对齐 - self.style_code.append(f"{style_indent}textalign 0.5") + self.style_code.append(f"{style_indent}textalign {xalign}") # 粗体、斜体、下划线、删除线 if fgui_text.bold: self.style_code.append(f"{style_indent}bold {fgui_text.bold}") @@ -383,7 +566,7 @@ def get_child_type(displayable): else: return DisplayableChildType.OTHER - def generate_button_children(self, fgui_button): + def generate_button_children(self, fgui_button : FguiButton): """ 生成按钮各状态的子组件。 """ @@ -428,18 +611,22 @@ def generate_button_children(self, fgui_button): # 将displayable_list中的子组件按状态分别添加到对应列表中 for displayable in fgui_button.display_list.displayable_list: + displayable_id = '' + if isinstance(displayable, FguiGraph): + displayable_id = f"{fgui_button.name}_{displayable.id}" + else: + displayable_id = displayable.id if displayable.gear_display is None: - state_children_dict['always_show'].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable))) + state_children_dict['always_show'].append((displayable_id, FguiToRenpyConverter.get_child_type(displayable))) break for i in range(0, state_number): if displayable.gear_display and str(i) in displayable.gear_display.controller_index: - state_children_dict[state_index_name_dict[i]].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable))) + state_children_dict[state_index_name_dict[i]].append((displayable_id, FguiToRenpyConverter.get_child_type(displayable))) continue # print(state_children_dict) return state_children_dict - def generate_button_screen(self, component): """ @@ -470,7 +657,7 @@ def generate_button_screen(self, component): self.screen_ui_code.clear() self.has_dismiss = False self.dismiss_action_list.clear() - self.style_code.clear() + # self.style_code.clear() # 4种状态的子组件列表 idle_child_list = [] @@ -554,7 +741,7 @@ def generate_button_screen(self, component): break # print(image_id_name_mapping) elif isinstance(displayable, FguiGraph): - self.renpy_code.extend(self.generate_graph_definitions(displayable)) + self.renpy_code.extend(self.generate_graph_definitions(displayable, button_name)) # break # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。 # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 @@ -568,15 +755,15 @@ def generate_button_screen(self, component): # 根据state_children_dict生成各种对应状态的background # idle_background - self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle']) + self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name) # selected_background - self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected']) + self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name) # hover_background - self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'],) + self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name) # selected_hover_background - self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover']) + self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name) # insensitive_background - self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive']) + self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name) # 重置缩进级别 @@ -592,17 +779,17 @@ def generate_button_screen(self, component): self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'") self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})") - # self.screen_ui_code.append(f"{self.indent_str}background '{background}'") - if state_children_dict['idle']: - self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'") - if state_children_dict['selected']: - self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'") - if state_children_dict['hover']: - self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'") - if state_children_dict['selected_hover']: - self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'") - if state_children_dict['insensitive']: - self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'") + self.screen_ui_code.append(f"{self.indent_str}background '{button_name}_[prefix_]background'") + # if state_children_dict['idle']: + # self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'") + # if state_children_dict['selected']: + # self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'") + # if state_children_dict['hover']: + # self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'") + # if state_children_dict['selected_hover']: + # self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'") + # if state_children_dict['insensitive']: + # self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'") self.screen_ui_code.append(f"{self.indent_str}text title") self.screen_ui_code.append(f"{self.indent_str}action actions") # 一些按钮特性 @@ -622,7 +809,7 @@ def generate_button_screen(self, component): def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"): return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5) - def generate_image_object(self, image_name, displayable_list): + def generate_image_object(self, image_name : str, displayable_list : list, component_name: str): """ 生成image对象,入参为一些子组件列表,自带transform。 暂时不考虑允许列表中的子组件添加额外transform,只单纯堆叠。 @@ -643,7 +830,7 @@ def generate_image_object(self, image_name, displayable_list): name = self.fgui_assets.get_componentname_by_id(displayable.src) image_definitions.append(f"{self.indent_str}'{name}'") elif displayalbe_type == DisplayableChildType.GRAPH: - image_definitions.append(f"{self.indent_str}'{displayable.id}'") + image_definitions.append(f"{self.indent_str}'{component_name}_{displayable.id}'") elif displayalbe_type == DisplayableChildType.TEXT: # image_definitions.append(f"{self.indent_str}Text({displayable.text})") text_displayable_string = self.generate_text_displayable_string(displayable) @@ -661,6 +848,9 @@ def generate_image_object(self, image_name, displayable_list): @staticmethod def generate_text_outline_string(fgui_text): outline_string = '[]' + if not isinstance(fgui_text, FguiText): + print("It is not a text Displayable.") + return outline_string has_shadow = fgui_text.shadow_color has_outline = fgui_text.stroke_color if has_shadow: @@ -693,7 +883,9 @@ def generate_text_displayable_string(self, fgui_text): text_font_param = f"font='{text_font}'" text_font_size_param = f"size={fgui_text.font_size}" text_font_color_param = f"color='{fgui_text.text_color}'" - text_textalign_param = f"textalign=0.5" + text_min_width_param = f"min_width={fgui_text.size[0]}" + xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) + text_textalign_param = f"textalign={xalign}" text_bold_param = f"bold={fgui_text.bold}" text_italic_param = f"italic={fgui_text.italic}" text_underline_param = f"underline={fgui_text.underline}" @@ -702,7 +894,7 @@ def generate_text_displayable_string(self, fgui_text): text_outline_string = self.generate_text_outline_string(fgui_text) text_outlines_parame = 'outlines={text_outline_string}' - text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" + text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" return text_displayable_string def generate_image_displayable(self, fgui_image): @@ -752,9 +944,10 @@ def generate_image_displayable(self, fgui_image): break return image_code - def generate_graph_displayable(self, fgui_graph): + def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : str) -> list: """ - 生成图形组件。图形组件有多种类别: + 生成图形组件。用于screen中。 + 图形组件有多种类别: None: 空白 rect: 矩形(可带圆角) eclipse: 椭圆(包括圆形) @@ -767,8 +960,8 @@ def generate_graph_displayable(self, fgui_graph): print("It is not a graph displayable.") return graph_code - self.renpy_code.extend(self.generate_graph_definitions(fgui_graph)) - graph_code.append(f"{self.indent_str}add '{fgui_graph.id}'") + self.renpy_code.extend(self.generate_graph_definitions(fgui_graph, component_name)) + graph_code.append(f"{self.indent_str}add '{component_name}_{fgui_graph.id}'") return graph_code @@ -805,7 +998,10 @@ def generate_text_displayable(self, fgui_text): self.indent_level_up() text_code.append(f"{self.indent_str}action {fgui_text.name}_input_value.Enable()") self.indent_level_down() - + # message 表示可能需要接收Ren'Py的提示语 + elif fgui_text.name == 'message': + text_code.append(f"{self.indent_str}text message:") + print("Detected a 'message' text displayable. Please ensure it was used in confirm screen.") else: text_code.append(f"{self.indent_str}text '{text_str}':") self.indent_level_up() @@ -852,6 +1048,7 @@ def generate_text_displayable(self, fgui_text): text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}") if fgui_text.leading: text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}") + text_code.append(f"{self.indent_str}min_width {fgui_text.size[0]}") xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) text_code.append(f"{self.indent_str}textalign {xalign}") if fgui_text.bold: @@ -888,6 +1085,8 @@ def generate_text_displayable(self, fgui_text): if fgui_text.leading: text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}") + #设置最小宽度才能使text_align生效 + text_code.append(f"{self.indent_str}min_width {fgui_text.size[0]}") # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置 xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) text_code.append(f"{self.indent_str}textalign {xalign}") @@ -1055,7 +1254,7 @@ def generate_renpy_code(self): elif component.extention == 'Label': pass elif component.extention == 'Slider': - pass + self.generate_slider_style(component) elif component.extention == 'ComboBox': pass elif component.extention == 'ProgressBar': @@ -1064,6 +1263,8 @@ def generate_renpy_code(self): self.generate_screen(component) self.renpy_code.extend(self.screen_code) + self.renpy_code.extend(self.style_code) + def save_to_file(self, filename): """ 保存Ren'Py代码 From a8d78a9219179c9558bf4fda6b96b4258e6a9967 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Sun, 9 Nov 2025 11:48:44 +0800 Subject: [PATCH 10/19] Support sliders in screen --- .../utils/renpy/Fgui2RenpyConverter.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index f64a425..fe1ff94 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -359,6 +359,8 @@ def generate_slider_style(self, fgui_slider : FguiSlider): style_definition_code.append(f"{self.indent_unit}thumb Fixed(Frame('{fgui_slider.name}_[prefix_]thumb_image',xysize={grip_com.size},pos=({thumb_xpos},{thumb_ypos})))") style_definition_code.append("") + # 添加头部注释 + slider_style_code.append("# 滑动条样式定义") slider_style_code.extend(bar_image_definition_code) slider_style_code.extend(style_definition_code) @@ -460,6 +462,16 @@ def generate_screen(self, component): parameter_str = self.generate_button_parameter(None, displayable.custom_data) self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'") self.indent_level_down() + continue + # 滑动条 + if ref_com.extention == "Slider" and ref_com.name != None: + self.screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + bar_value = displayable.custom_data if displayable.custom_data else displayable.slider_property.current_value + self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'") + self.indent_level_down() + continue self.screen_code.extend(self.screen_definition_head) if self.screen_variable_code: From 33634fc461b682100ee9877014dadbce17f9a4d8 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Tue, 11 Nov 2025 16:01:30 +0800 Subject: [PATCH 11/19] Add special screens (choice, say) converting method. --- src/fgui_converter/FguiAssetsParseLib.py | 16 +- .../utils/renpy/Fgui2RenpyConverter.py | 216 +++++++++++++++++- .../renpy_ellipseAA_template.txt | 2 +- .../renpy_ellipse_template.txt | 2 +- .../renpy_rectangleAA_template.txt | 2 +- .../renpy_rectangle_template.txt | 2 +- 6 files changed, 223 insertions(+), 17 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index 7be9ef2..c12f46b 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -366,6 +366,7 @@ def __init__(self, component_etree, id, name, package_desc_id=None): class FguiDisplayList: """ FairyGUI组件内部显示列表,xml中displayList标签内容。 + 只要一个组件不为空组件,必定会有displayList。 """ def __init__(self, display_list_etree, package_desc_id=None): self.display_list_etree = display_list_etree @@ -665,12 +666,25 @@ def __init__(self, display_item_tree): class FguiImage(FguiDisplayable): """ FairyGUI中的图片。 - 自身没有什么独特属性。 + tag为image。 + 属性如下: + color-颜色:一个6位Hex字符串,表示显示时所有像素的RGA都要乘以该值 + flip-翻转类型:"hz"-水平、"vt"-垂直、"both"-水平+垂直。 + fillMethod-填充方式:"hz"-水平、"vt"-垂直、"radial90"-90度、"radial180"-180度、"radial360"-360度 + fillOrigin-填充原点:0(默认值)、1、2、3。该值根据不同的填充方式有不同的含义。 + fillAmount-填充比例:100(默认值),一个介于0到100之间的整数。 + 样例: + """ def __init__(self, display_item_tree): if display_item_tree.tag != "image" : raise ValueError("xml tag is not image.") super().__init__(display_item_tree) + self.multiply_color = self.display_item_tree.get("color", "#ffffff") + self.flip_type = self.display_item_tree.get("flip") + self.fill_method = self.display_item_tree.get("fillMethod") + self.fill_origin = self.display_item_tree.get("fillOrigin") + self.fill_amount = self.display_item_tree.get("fillAmount") class FguiListItem(): """ diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index fe1ff94..6d74103 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -9,7 +9,6 @@ import shutil from enum import IntEnum -from tkinter import HORIZONTAL, NO, VERTICAL # 复合型组件的子组件枚举类型 class DisplayableChildType(IntEnum): @@ -51,7 +50,11 @@ class FguiToRenpyConverter: # 模态类界面名称,需要添加 Modal True。 modal_screen_name_list = ['confirm'] + # 选项分支界面。 + choice_screen_name_list = ['choice'] + # 对话界面 + say_screen_name_list = ['say'] def __init__(self, fgui_assets): self.fgui_assets = fgui_assets @@ -360,8 +363,8 @@ def generate_slider_style(self, fgui_slider : FguiSlider): style_definition_code.append("") # 添加头部注释 - slider_style_code.append("# 滑动条样式定义") slider_style_code.extend(bar_image_definition_code) + slider_style_code.append("# 滑动条样式定义") slider_style_code.extend(style_definition_code) self.style_code.extend(slider_style_code) @@ -375,6 +378,14 @@ def is_menu_screen(screen_name): def is_modal_screen(screen_name): return screen_name in FguiToRenpyConverter.modal_screen_name_list + @staticmethod + def is_choice_screen(screen_name): + return screen_name in FguiToRenpyConverter.choice_screen_name_list + + @staticmethod + def is_say_screen(screen_name): + return screen_name in FguiToRenpyConverter.say_screen_name_list + def generate_screen(self, component): """ 生成screen定义。目标样例: @@ -410,7 +421,7 @@ def generate_screen(self, component): self.screen_has_dismiss = False self.dismiss_action_list.clear() - self.screen_definition_head.append("# 组件Screen") + self.screen_definition_head.append("# 界面定义") self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来") id = component.id @@ -418,6 +429,16 @@ def generate_screen(self, component): # 界面入参列表 screen_params = '' + # choice界面的特殊处理 + if self.is_choice_screen(screen_name): + self.generate_choice_screen(component) + return + + # say界面的特殊处理 + if self.is_say_screen(screen_name): + self.generate_say_screen(component) + return + # confirm 界面固定入参 if screen_name == 'confirm': screen_params = 'message, yes_action, no_action' @@ -468,7 +489,14 @@ def generate_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}fixed:") self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") - bar_value = displayable.custom_data if displayable.custom_data else displayable.slider_property.current_value + # 若在自定义数据中指定了关联数据对象,则直接使用。 + if displayable.custom_data: + bar_value = displayable.custom_data + # 若未指定则在screen中生成一个临时变量 + else: + variable_name = f"{screen_name}_{displayable.name}_barvalue" + self.screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}") + bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value) self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'") self.indent_level_down() continue @@ -491,9 +519,164 @@ def generate_screen(self, component): self.screen_ui_code.append("") self.screen_code.extend(self.screen_ui_code) + def generate_choice_screen(self, component): + print("This is choice screen.") + caption_text = None + choice_button = None + choice_list = None + + for displayable in component.display_list.displayable_list: + # 生成标题文本样式 + if displayable.name == 'caption' and isinstance(displayable, FguiText): + self.generate_text_style(displayable, "choice_caption_text_style") + caption_text = displayable + # 查找第一个列表 + if isinstance(displayable, FguiList) and choice_list == None: + choice_list = displayable + choice_button = self.fgui_assets.get_component_by_id(displayable.default_item_id) + + # 检查查找结果 + if choice_list == None: + print("Lack of choice button list.") + return + if not isinstance(choice_button, FguiButton): + print("Choice button list's item is not Button.") + return + + # vbox的行距 + vbox_spacing = choice_list.line_gap if choice_list else 0 + screen_params = 'items' + self.reset_indent_level() + self.screen_definition_head.append(f"screen {component.name}({screen_params}):") + self.indent_level_up() + # 选项菜单标题 + self.screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {caption_text.xypos}") + self.screen_ui_code.append(f"{self.indent_str}for i in items:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}if not i.action:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}text i.caption style 'choice_caption_text_style'") + self.indent_level_down(3) + # 选项菜单按钮列表 + self.screen_ui_code.append(f"{self.indent_str}vbox:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}spacing {vbox_spacing}") + self.screen_ui_code.append(f"{self.indent_str}pos {choice_list.xypos}") + self.screen_ui_code.append(f"{self.indent_str}for i in items:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}if i.action:") + self.indent_level_up() + parameter_str = f"title=i.caption, actions=i.action" + self.screen_ui_code.append(f"{self.indent_str}use {choice_button.name}({parameter_str})") + self.reset_indent_level() + + self.screen_ui_code.append("") + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + return + def generate_say_screen(self, component): + print("This is say screen.") + who_text = None + what_text = None + namebox = None + textbox = None + namebox_image = 'Null()' + textbox_image = 'Null()' + namebox_pos = (0, 0) + textbox_pos = (0, 0) - def generate_button_parameter(self, button_title=None, original_actions_str=None): + for displayable in component.display_list.displayable_list: + # 生成发言角色名的文本样式 + if displayable.name == 'who' and isinstance(displayable, FguiText): + self.generate_text_style(displayable, "say_who_text_style") + who_text = displayable + # 生成发言内容的文本样式 + if displayable.name == 'what' and isinstance(displayable, FguiText): + self.generate_text_style(displayable, "say_what_text_style") + what_text = displayable + # 角色名的背景 + if displayable.name == 'namebox' and isinstance(displayable, FguiImage): + namebox = self.fgui_assets.get_component_by_id(displayable.src) + namebox_pos = displayable.xypos + namebox_image = f"'{self.get_image_name(displayable)}'" + + # 发言内容的背景 + if displayable.name == 'textbox' and isinstance(displayable, FguiImage): + textbox = self.fgui_assets.get_component_by_id(displayable.src) + textbox_pos = displayable.xypos + textbox_image = f"'{self.get_image_name(displayable)}'" + + # 检查查找结果 + if who_text == None: + print("Lack of who text component.") + return + if what_text == None: + print("Lack of what text component.") + return + if namebox: + print("Namebox background is Null.") + if textbox: + print("Textbox background is Null.") + + screen_params = 'who, what' + self.reset_indent_level() + # say界面需要覆盖默认gui设置 + self.screen_definition_head.append("# say界面") + self.screen_definition_head.append("style say_label is say_who_text_style") + self.screen_definition_head.append("style say_dialogue is say_what_text_style") + self.screen_definition_head.append(f"screen {component.name}({screen_params}):") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}if who is not None:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}add {namebox_image}:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {namebox_pos}") + self.indent_level_down() + self.screen_ui_code.append(f"{self.indent_str}text who id 'who' style 'say_who_text_style':") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}") + self.indent_level_down(2) + self.screen_ui_code.append(f"{self.indent_str}add {textbox_image}:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {textbox_pos}") + self.indent_level_down() + self.screen_ui_code.append(f"{self.indent_str}text what id 'what' style 'say_what_text_style':") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {what_text.xypos}") + self.reset_indent_level() + + self.screen_ui_code.append("") + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + return + + # 根据图片组件对象获取图片名 + def get_image_name(self, fgui_image): + if not isinstance(fgui_image, FguiImage): + print("It is not a Image object.") + return None + for image in self.fgui_assets.package_desc.image_list: + if fgui_image.src == image.id: + return image.name + + @staticmethod + def generate_variable_definition_str(variable_name, current_value=None): + return f"default {variable_name} = {current_value}" + + @staticmethod + def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100, current_value=0, scope='local'): + barvalue_str = '' + barvalue_scope_str = '' + if scope in ('local', 'screen'): + barvalue_scope_str = scope.capitalize() + barvalue_str = f"{barvalue_scope_str}VariableValue('{barvalue_name}',min={min_value},max={max_value})" + return barvalue_str + + @staticmethod + def generate_button_parameter(button_title=None, original_actions_str=None): parameter_str = "" title_str = "" actions_str = "" @@ -529,7 +712,7 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str): return # 样式具有固定一档的缩进 style_indent = " " - # default_title = fgui_text.text + self.style_code.append(f"# 文本{fgui_text.name}样式定义") # 定义样式 self.style_code.append(f"style {style_name}:") self.style_code.append(f"{style_indent}xysize {fgui_text.size}") @@ -542,8 +725,8 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str): self.style_code.append(f"{style_indent}font '{text_font}'") self.style_code.append(f"{style_indent}size {fgui_text.font_size}") self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'") - xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) - self.style_code.append(f"{style_indent}align ({xalign}, {yalign})") + # xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) + # self.style_code.append(f"{style_indent}align ({xalign}, {yalign})") text_outline_string = self.generate_text_outline_string(fgui_text) self.style_code.append(f"{style_indent}outlines {text_outline_string}") @@ -669,7 +852,7 @@ def generate_button_screen(self, component): self.screen_ui_code.clear() self.has_dismiss = False self.dismiss_action_list.clear() - # self.style_code.clear() + self.style_code.clear() # 4种状态的子组件列表 idle_child_list = [] @@ -719,7 +902,7 @@ def generate_button_screen(self, component): return # 生成按钮组件screen - self.screen_definition_head.append("# 按钮组件Screen") + self.screen_definition_head.append("# 按钮screen定义") self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来") id = component.id @@ -728,6 +911,8 @@ def generate_button_screen(self, component): background = self.default_background default_title = '' + title_displayable = None + text_xalign, text_yalign = 0, 0 for displayable in component.display_list.displayable_list: # 不带显示控制器表示始终显示 @@ -761,9 +946,10 @@ def generate_button_screen(self, component): # 重置缩进级别 self.reset_indent_level() default_title = displayable.text + title_displayable = displayable + text_xalign, text_yalign = self.trans_text_align(displayable.align, displayable.v_align) # 定义样式 self.generate_text_style(displayable, f"{button_name}_text") - # self.screen_code.extend(self.style_code) # 根据state_children_dict生成各种对应状态的background # idle_background @@ -802,7 +988,11 @@ def generate_button_screen(self, component): # self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'") # if state_children_dict['insensitive']: # self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'") - self.screen_ui_code.append(f"{self.indent_str}text title") + self.screen_ui_code.append(f"{self.indent_str}text title:") + # Ren'Py中没有文本相对自身组件的垂直对齐方式,尝试用整个文本组件的对齐来凑合。 + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})") + self.indent_level_down() self.screen_ui_code.append(f"{self.indent_str}action actions") # 一些按钮特性 # focus_mask,对应FGUI中的点击“测试”。 @@ -947,6 +1137,8 @@ def generate_image_displayable(self, fgui_image): image_code.append(f"{self.indent_str}rotate {fgui_image.rotation}") if fgui_image.alpha != 1.0: image_code.append(f"{self.indent_str}alpha {fgui_image.alpha}") + if fgui_image.multiply_color != "#ffffff": + image_code.append(f"{self.indent_str}matrixcolor TintMatrix('{fgui_image.multiply_color}')") if fgui_image.scale != (1.0, 1.0): image_code.append(f"{self.indent_str}xzoom {fgui_image.scale[0]} yzoom {fgui_image.scale[1]}") # 九宫格或平铺图片需要指定尺寸 diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt index cd6a884..6777d81 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt @@ -1,4 +1,4 @@ -# 抗锯齿的椭圆(圆形)图像定义 +# 抗锯齿的椭圆(圆形)图形定义 image {image_name}: Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01) xysize {xysize} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt index 364e171..25aaa92 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt @@ -1,4 +1,4 @@ -# 椭圆(圆形)图像定义 +# 椭圆(圆形)图形定义 image {image_name}: Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}) xysize {xysize} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt index 75237be..f795118 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt @@ -1,4 +1,4 @@ -# 带光滑渐变的圆角矩形图像定义 +# 带光滑渐变的圆角矩形图形定义 image {image_name}: Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0) xysize {xysize} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt index eb60024..b58d746 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt @@ -1,4 +1,4 @@ -# 圆角矩形图像定义 +# 圆角矩形图形定义 image {image_name}: Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}) xysize {xysize} From 82b2e345641dbf7d9019e4f8ac078530c7b4e354 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Wed, 12 Nov 2025 00:07:23 +0800 Subject: [PATCH 12/19] Some component, as button and slider, could get custom data from source component configure. --- src/fgui_converter/FguiAssetsParseLib.py | 2 ++ .../utils/renpy/Fgui2RenpyConverter.py | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index c12f46b..dfe8897 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -263,6 +263,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None): # 点击测试区域,包含3个字段,分别为引用源src和组件内相对坐标x、y。 hit_test = component_etree.get("hitTest") self.hit_test = FguiHitTest(hit_test) if hit_test else None + # 自定义数据。实际使用时,相同id的FguiDisplayable中的自定义数据优先。 + self.custom_data = component_etree.get("customData") # 控制器,一般不超过1个。 self.controller_list = [] diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 6d74103..047f86c 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -45,7 +45,7 @@ class FguiToRenpyConverter: default_background = '#fff' # 菜单类界面名称,需要添加 tag menu。 - menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences'] + menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about'] # 模态类界面名称,需要添加 Modal True。 modal_screen_name_list = ['confirm'] @@ -476,11 +476,13 @@ def generate_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}fixed:") self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。 + actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 if displayable.button_property: - parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data) + parameter_str = self.generate_button_parameter(displayable.button_property.title, actions) else: - parameter_str = self.generate_button_parameter(None, displayable.custom_data) + parameter_str = self.generate_button_parameter(None, actions) self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'") self.indent_level_down() continue @@ -492,6 +494,9 @@ def generate_screen(self, component): # 若在自定义数据中指定了关联数据对象,则直接使用。 if displayable.custom_data: bar_value = displayable.custom_data + # 否则再查找引用源对象的自定义数据 + elif ref_com.custom_data: + bar_value = ref_com.custom_data # 若未指定则在screen中生成一个临时变量 else: variable_name = f"{screen_name}_{displayable.name}_barvalue" @@ -966,7 +971,8 @@ def generate_button_screen(self, component): # 重置缩进级别 self.reset_indent_level() - self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):") + default_actions = component.custom_data if component.custom_data else 'NullAction()' + self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}):") self.indent_level_up() # 如果按钮有按下效果,添加自定义组件 if component.button_down_effect: @@ -1040,6 +1046,9 @@ def generate_image_object(self, image_name : str, displayable_list : list, compo # 其他类型暂时用空对象占位 else: image_definitions.append(f"{self.indent_str}Null()") + image_definitions.append(f"{self.indent_str}pos {displayable.xypos}") + # TODO 其他transform property还待添加 + # 也可考虑检查displayList中的子组件,将非默认值值生成在一个额外的容器中,便于用循环生成脚本。 self.indent_level_down() image_definitions.append("") From 616a52cee936672226c9e1dcf3166237062e1de2 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Wed, 12 Nov 2025 23:02:00 +0800 Subject: [PATCH 13/19] Add special screens (say, save, load) converting method. 1. Optimized screen generating method. 2. Add special screens converting. 3. Fixed rectangle shader transparent issue. --- .../utils/renpy/Fgui2RenpyConverter.py | 399 ++++++++++++++---- .../renpy/renpy_templates/02_renpy_shader.rpy | 8 +- 2 files changed, 317 insertions(+), 90 deletions(-) diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 047f86c..fc460d7 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -55,6 +55,9 @@ class FguiToRenpyConverter: # 对话界面 say_screen_name_list = ['say'] + + # 存档相关界面 + save_load_screen_name_list = ['save', 'load'] def __init__(self, fgui_assets): self.fgui_assets = fgui_assets @@ -65,6 +68,9 @@ def __init__(self, fgui_assets): self.screen_function_code = [] self.screen_ui_code = [] self.style_code = [] + self.image_definition_code = [] + self.graph_definition_code = [] + self.game_global_variables_code = [] # dismiss用于取消一些组件的focus状态,例如input。 self.screen_has_dismiss = False @@ -88,6 +94,11 @@ def __init__(self, fgui_assets): self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt') self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt') + def set_game_global_variables(self, variable_name, variable_value): + variable_str = f"define {variable_name} = {variable_value}" + self.game_global_variables_code.append(variable_str) + self.game_global_variables_code.append('') + def calculate_indent(self): self.indent_str = self.indent_unit * self.root_indent_level return self.indent_str @@ -158,7 +169,7 @@ def generate_image_definitions(self): image_definitions.append(f'image {image_name} = im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))') image_definitions.append("") - self.renpy_code.extend(image_definitions) + self.image_definition_code.extend(image_definitions) def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : str) -> list: """ @@ -371,22 +382,95 @@ def generate_slider_style(self, fgui_slider : FguiSlider): @staticmethod - def is_menu_screen(screen_name): + def is_menu_screen(screen_name : str): return screen_name in FguiToRenpyConverter.menu_screen_name_list @staticmethod - def is_modal_screen(screen_name): + def is_modal_screen(screen_name : str): return screen_name in FguiToRenpyConverter.modal_screen_name_list @staticmethod - def is_choice_screen(screen_name): + def is_choice_screen(screen_name : str): return screen_name in FguiToRenpyConverter.choice_screen_name_list @staticmethod - def is_say_screen(screen_name): + def is_say_screen(screen_name : str): return screen_name in FguiToRenpyConverter.say_screen_name_list - def generate_screen(self, component): + @staticmethod + def is_save_load_screen(screen_name : str): + return screen_name in FguiToRenpyConverter.save_load_screen_name_list + + def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1): + screen_ui_code = [] + # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}") + end_index = len(component.display_list.displayable_list) if (list_end_index == -1) else list_end_index + for displayable in component.display_list.displayable_list[list_begin_index:end_index]: + # print(displayable.name) + # 图片组件 + if isinstance(displayable, FguiImage): + screen_ui_code.extend(self.generate_image_displayable(displayable)) + # 图形组件 + elif isinstance(displayable, FguiGraph): + screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name)) + # 文本组件 + elif isinstance(displayable, FguiText): + screen_ui_code.extend(self.generate_text_displayable(displayable)) + # 列表 + elif isinstance(displayable, FguiList): + screen_ui_code.extend(self.generate_list_displayable(displayable)) + # 装载器 + elif isinstance(displayable, FguiLoader): + pass + # 其他组件 + else: + # 根据引用源id查找组件 + ref_com = self.fgui_assets.get_component_by_id(displayable.src) + # 按钮。可设置标题,并根据自定义数据字段设置action。 + if ref_com.extention == "Button" and ref_com.name != None: + screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。 + actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data + # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 + if displayable.button_property: + parameter_str = self.generate_button_parameter(displayable.button_property.title, actions) + else: + parameter_str = self.generate_button_parameter(None, actions) + screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{component.name}_{displayable.id}'") + self.indent_level_down() + continue + # 滑动条 + if ref_com.extention == "Slider" and ref_com.name != None: + screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + # 若在自定义数据中指定了关联数据对象,则直接使用。 + if displayable.custom_data: + bar_value = displayable.custom_data + # 否则再查找引用源对象的自定义数据 + elif ref_com.custom_data: + bar_value = ref_com.custom_data + # 若未指定则在screen中生成一个临时变量 + else: + variable_name = f"{component.name}_{displayable.name}_barvalue" + screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}") + bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value) + screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'") + self.indent_level_down() + continue + # 其他组件 + screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") + screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'") + self.indent_level_down() + # print("convert_component_display_list method:") + # print(screen_ui_code) + return screen_ui_code + + def generate_screen(self, component : FguiComponent): """ 生成screen定义。目标样例: @@ -413,7 +497,7 @@ def generate_screen(self, component): pos (1007, 483) use main_menu_button(title='放弃', actions=Quit()) """ - self.screen_code.clear() + # self.screen_code.clear() self.screen_definition_head.clear() self.screen_variable_code.clear() self.screen_function_code.clear() @@ -439,6 +523,11 @@ def generate_screen(self, component): self.generate_say_screen(component) return + # save和load界面的特殊处理 + if self.is_save_load_screen(screen_name): + self.generate_save_load_screen(component) + return + # confirm 界面固定入参 if screen_name == 'confirm': screen_params = 'message, yes_action, no_action' @@ -448,63 +537,13 @@ def generate_screen(self, component): self.indent_level_up() if self.is_menu_screen(screen_name): self.screen_ui_code.append(f"{self.indent_str}tag menu\n") + # 若自定义了game_menu,则修改默认游戏内菜单显示的控制变量。 + if screen_name == "game_menu": + self.set_game_global_variables('_game_menu_screen', str("\"game_menu\"")) if self.is_modal_screen(screen_name): self.screen_ui_code.append(f"{self.indent_str}modal True") self.screen_ui_code.append(f"{self.indent_str}zorder 200\n") - for displayable in component.display_list.displayable_list: - # 图片组件 - if isinstance(displayable, FguiImage): - self.screen_ui_code.extend(self.generate_image_displayable(displayable)) - # 图形组件 - elif isinstance(displayable, FguiGraph): - self.screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name)) - # 文本组件 - elif isinstance(displayable, FguiText): - self.screen_ui_code.extend(self.generate_text_displayable(displayable)) - # 列表 - elif isinstance(displayable, FguiList): - self.screen_ui_code.extend(self.generate_list_displayable(displayable)) - # 装载器 - elif isinstance(displayable, FguiLoader): - pass - # 其他组件 - else: - # 根据引用源id查找组件 - ref_com = self.fgui_assets.get_component_by_id(displayable.src) - # 按钮。可设置标题,并根据自定义数据字段设置action。 - if ref_com.extention == "Button" and ref_com.name != None: - self.screen_ui_code.append(f"{self.indent_str}fixed:") - self.indent_level_up() - self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") - # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。 - actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data - # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 - if displayable.button_property: - parameter_str = self.generate_button_parameter(displayable.button_property.title, actions) - else: - parameter_str = self.generate_button_parameter(None, actions) - self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'") - self.indent_level_down() - continue - # 滑动条 - if ref_com.extention == "Slider" and ref_com.name != None: - self.screen_ui_code.append(f"{self.indent_str}fixed:") - self.indent_level_up() - self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") - # 若在自定义数据中指定了关联数据对象,则直接使用。 - if displayable.custom_data: - bar_value = displayable.custom_data - # 否则再查找引用源对象的自定义数据 - elif ref_com.custom_data: - bar_value = ref_com.custom_data - # 若未指定则在screen中生成一个临时变量 - else: - variable_name = f"{screen_name}_{displayable.name}_barvalue" - self.screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}") - bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value) - self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'") - self.indent_level_down() - continue + self.screen_ui_code.extend(self.convert_component_display_list(component)) self.screen_code.extend(self.screen_definition_head) if self.screen_variable_code: @@ -524,8 +563,14 @@ def generate_screen(self, component): self.screen_ui_code.append("") self.screen_code.extend(self.screen_ui_code) - def generate_choice_screen(self, component): + def generate_choice_screen(self, component : FguiComponent): print("This is choice screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() caption_text = None choice_button = None choice_list = None @@ -582,8 +627,142 @@ def generate_choice_screen(self, component): self.screen_code.extend(self.screen_ui_code) return - def generate_say_screen(self, component): + def generate_save_load_screen(self, component : FguiComponent): + print(f"This is {component.name} screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + slot_list = None + slot_list_index = -1 + default_slot_button = None + if component.display_list.displayable_list == None: + print(f"{component.name} contains no displayable.") + return + for i in range(len(component.display_list.displayable_list)): + displayable = component.display_list.displayable_list[i] + # 搜索名为 save_slot_list 的列表组件 + if displayable.name == 'save_slot_list' and isinstance(displayable, FguiList): + slot_list = displayable + slot_list_index = i + # 确认默认引用组件类型。 + default_slot_button = self.fgui_assets.get_component_by_id(slot_list.default_item_id) + break + if not slot_list: + print(f"{component.name} contains no slot list.") + return + # 检查slot_list默认引用组件类型是否为button + if not isinstance(default_slot_button, FguiButton): + print(f"{component.name} slot list item is not Button.") + return + + self.screen_definition_head.append("# 存档/读档 界面") + self.screen_definition_head.append(f"screen {component.name}():") + # save_slot_list 之前的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=slot_list_index)) + + # save_slot_list的处理 + slot_list_code = [] + item_number = len(slot_list.item_list) + + # 根据“溢出处理”是否可见区分处理。 + # 若“可见”,则使用hbox、vbox和grid。 + if slot_list.overflow == "visible": + # 单列竖排,使用vbox + if slot_list.layout == "column": + slot_list_code.append(f"{self.indent_str}vbox:") + # 单行横排,使用hbox + elif slot_list.layout == "row": + slot_list_code.append(f"{self.indent_str}hbox:") + # 其他,使用grid + else: + slot_list_code.append(f"{self.indent_str}grid {slot_list.line_item_count} {slot_list.line_item_count2}:") + + else: + slot_list_code.append(f"{self.indent_str}vpgrid:") + self.indent_level_up() + + # 若“隐藏”,使用不可滚动的vpgrid + if slot_list.overflow == "hidden": + slot_list_code.append(f"{self.indent_str}draggable False") + # 单列竖排 + if slot_list.layout == "column": + cols = 1 + rows = item_number + # 单行横排 + elif slot_list.layout == "row": + cols = item_number + rows = 1 + # 其他 + else: + cols = slot_list.line_item_count + rows = slot_list.line_item_count2 + slot_list_code.append(f"{self.indent_str}cols {cols}") + slot_list_code.append(f"{self.indent_str}rows {rows}") + # 若“滚动”,使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。 + elif slot_list.overflow == "scroll": + slot_list_code.append(f"{self.indent_str}draggable True") + # 垂直滚动 + if slot_list.scroll == "vertical": + pass + # 水平滚动 + elif slot_list.scroll == "horizontal": + pass + # 自由滚动 + elif slot_list.scroll == "both": + pass + # 单列竖排,使用vbox + if slot_list.layout == "column": + cols = 1 + rows = item_number + # 单行横排,使用hbox + elif slot_list.layout == "row": + cols = item_number + rows = 1 + # 其他,使用grid + else: + cols = slot_list.line_item_count + rows = slot_list.line_item_count2 + slot_list_code.append(f"{self.indent_str}cols {cols}") + slot_list_code.append(f"{self.indent_str}rows {rows}") + if slot_list.line_gap: + slot_list_code.append(f"{self.indent_str}yspacing {slot_list.line_gap}") + if slot_list.col_gap: + slot_list_code.append(f"{self.indent_str}xspacing {slot_list.col_gap}") + self.indent_level_down() + + self.indent_level_up() + slot_list_code.append(f"{self.indent_str}pos {slot_list.xypos}") + slot_list_code.append(f"{self.indent_str}xysize {slot_list.size}") + if slot_list.margin: + slot_list_code.append(f"{self.indent_str}margin {slot_list.margin}") + # 添加元素 + for i in range(item_number): + # 值根据列表长度添加对应数量的默认元素,即存档按钮 + parameter_str = self.generate_button_callable_parameter(f"FileTime({i+1}, format=_(\"{{#file_time}}%Y-%m-%d %H:%M\"), empty=_(''))", f"FileAction({i+1})", f"FileScreenshot({i+1})") + slot_list_code.append(f"{self.indent_str}use {default_slot_button.name}({parameter_str})") + + self.indent_level_down() + self.screen_ui_code.extend(slot_list_code) + + # save_slot_list 之后的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=slot_list_index+1)) + self.screen_ui_code.append("") + + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + return + + def generate_say_screen(self, component : FguiComponent): print("This is say screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() who_text = None what_text = None namebox = None @@ -681,19 +860,35 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100, return barvalue_str @staticmethod - def generate_button_parameter(button_title=None, original_actions_str=None): + def generate_button_parameter(button_title=None, original_actions_str=None, icon=None): parameter_str = "" - title_str = "" - actions_str = "" + title_str = "title=''" + actions_str = "actions=None" + icon_str = "icon=Null()" if button_title : - title_str = f"title=\'{button_title}\'".replace("\n", "\\n").replace("\r", "\\n") + title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n") if original_actions_str : actions_str = f"actions={original_actions_str}" - if button_title and original_actions_str: - parameter_str = f"{title_str}, {actions_str}" - else: - parameter_str = title_str if title_str else actions_str + if icon: + icon_str = f"icon={icon}" + + parameter_str = f"{title_str}, {actions_str}, {icon_str}" + return parameter_str + + @staticmethod + def generate_button_callable_parameter(button_title=None, actions=None, icon=None): + parameter_str = "" + title_str = "title=''" + actions_str = "actions=None" + icon_str = "icon=Null()" + if button_title : + title_str = f"title={button_title}".replace("\n", "\\n").replace("\r", "\\n") + if actions: + actions_str = f"actions={actions}" + if icon: + icon_str = f"icon={icon}" + parameter_str = f"{title_str}, {actions_str}, {icon_str}" return parameter_str def generate_text_style(self, fgui_text : FguiText, style_name : str): @@ -834,6 +1029,7 @@ def generate_button_screen(self, component): screen main_menu_button(title='', actions=NullAction()): button: + padding (0, 0, 0, 0) xysize (273, 61) style_prefix 'main_menu_button' background 'main_menu_button_bg' @@ -850,14 +1046,14 @@ def generate_button_screen(self, component): textalign 0.5 """ - self.screen_code.clear() + # self.screen_code.clear() self.screen_definition_head.clear() self.screen_variable_code.clear() self.screen_function_code.clear() self.screen_ui_code.clear() self.has_dismiss = False self.dismiss_action_list.clear() - self.style_code.clear() + # self.style_code.clear() # 4种状态的子组件列表 idle_child_list = [] @@ -918,6 +1114,12 @@ def generate_button_screen(self, component): default_title = '' title_displayable = None text_xalign, text_yalign = 0, 0 + title_pos = (0, 0) + title_anchor = (0, 0) + + icon_pos = (0, 0) + icon_size = (0, 0) + icon_image = None for displayable in component.display_list.displayable_list: # 不带显示控制器表示始终显示 @@ -941,10 +1143,8 @@ def generate_button_screen(self, component): if displayable.src == image.id: image_id_name_mapping[displayable.id] = image.name break - # print(image_id_name_mapping) elif isinstance(displayable, FguiGraph): - self.renpy_code.extend(self.generate_graph_definitions(displayable, button_name)) - # break + self.graph_definition_code.extend(self.generate_graph_definitions(displayable, button_name)) # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。 # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同,Ren'Py的效果更好。 elif isinstance(displayable, FguiText) and displayable.name == 'title': @@ -953,8 +1153,16 @@ def generate_button_screen(self, component): default_title = displayable.text title_displayable = displayable text_xalign, text_yalign = self.trans_text_align(displayable.align, displayable.v_align) + title_pos = displayable.xypos + title_anchor = displayable.pivot # 定义样式 self.generate_text_style(displayable, f"{button_name}_text") + # icon组件仅作为一个可从按钮外部传入额外可视组件的入口。 + elif isinstance(displayable, FguiLoader) and displayable.name == 'icon': + icon_pos = displayable.xypos + icon_size = displayable.size + icon_image = displayable.item_url + pass # 根据state_children_dict生成各种对应状态的background # idle_background @@ -972,7 +1180,10 @@ def generate_button_screen(self, component): # 重置缩进级别 self.reset_indent_level() default_actions = component.custom_data if component.custom_data else 'NullAction()' - self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}):") + param_str = self.generate_button_parameter(default_title, default_actions, icon_image) + self.screen_definition_head.append(f"screen {button_name}({param_str}):") + # self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}, icon=Null()):") + self.indent_level_up() # 如果按钮有按下效果,添加自定义组件 if component.button_down_effect: @@ -981,8 +1192,10 @@ def generate_button_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}pressed_{component.button_down_effect} {component.button_down_effect_value}") self.screen_ui_code.append(f"{self.indent_str}button:") self.indent_level_up() + # 默认button样式的padding为(6,6,6,6),可能导致部分子组件位置变化。 + self.screen_ui_code.append(f"{self.indent_str}padding (0, 0, 0, 0)") self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'") - self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})") + self.screen_ui_code.append(f"{self.indent_str}xysize {xysize}") self.screen_ui_code.append(f"{self.indent_str}background '{button_name}_[prefix_]background'") # if state_children_dict['idle']: # self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'") @@ -997,9 +1210,17 @@ def generate_button_screen(self, component): self.screen_ui_code.append(f"{self.indent_str}text title:") # Ren'Py中没有文本相对自身组件的垂直对齐方式,尝试用整个文本组件的对齐来凑合。 self.indent_level_up() - self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})") + # self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})") + self.screen_ui_code.append(f"{self.indent_str}pos {title_pos}") + self.screen_ui_code.append(f"{self.indent_str}anchor {title_anchor}") self.indent_level_down() self.screen_ui_code.append(f"{self.indent_str}action actions") + # 在最上层加上icon + self.screen_ui_code.append(f"{self.indent_str}add icon:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {icon_pos}") + self.screen_ui_code.append(f"{self.indent_str}size {icon_size}") + self.indent_level_down() # 一些按钮特性 # focus_mask,对应FGUI中的点击“测试”。 if component.hit_test: @@ -1009,10 +1230,10 @@ def generate_button_screen(self, component): self.screen_ui_code.append("") - self.screen_code.extend(self.style_code) + # self.screen_code.extend(self.style_code) self.screen_code.extend(self.screen_definition_head) self.screen_code.extend(self.screen_ui_code) - self.renpy_code.extend(self.screen_code) + # self.renpy_code.extend(self.screen_code) def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"): return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5) @@ -1053,7 +1274,7 @@ def generate_image_object(self, image_name : str, displayable_list : list, compo image_definitions.append("") - self.renpy_code.extend(image_definitions) + self.image_definition_code.extend(image_definitions) self.root_indent_level = indent_level @staticmethod @@ -1108,7 +1329,7 @@ def generate_text_displayable_string(self, fgui_text): text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" return text_displayable_string - def generate_image_displayable(self, fgui_image): + def generate_image_displayable(self, fgui_image : FguiImage): """ 生成图片组件。 前提为image对象的定义已经在generate_image_definitions中生成。 @@ -1173,7 +1394,7 @@ def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : st print("It is not a graph displayable.") return graph_code - self.renpy_code.extend(self.generate_graph_definitions(fgui_graph, component_name)) + self.graph_definition_code.extend(self.generate_graph_definitions(fgui_graph, component_name)) graph_code.append(f"{self.indent_str}add '{component_name}_{fgui_graph.id}'") return graph_code @@ -1474,9 +1695,12 @@ def generate_renpy_code(self): pass else: self.generate_screen(component) - self.renpy_code.extend(self.screen_code) - + + self.renpy_code.extend(self.game_global_variables_code) + self.renpy_code.extend(self.image_definition_code) + self.renpy_code.extend(self.graph_definition_code) self.renpy_code.extend(self.style_code) + self.renpy_code.extend(self.screen_code) def save_to_file(self, filename): """ @@ -1525,6 +1749,9 @@ def cleanup(self): self.screen_ui_code.clear() self.style_code.clear() self.dismiss_action_list.clear() + self.graph_definition_code.clear() + self.image_definition_code.clear() + self.game_global_variables_code.clear() # 重置缩进相关状态 self.root_indent_level = 0 diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy index c14bf28..2256c87 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy +++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy @@ -19,9 +19,9 @@ init python: vec2 uv = v_tex_coord - vec2(0.5, 0.5); vec2 tex_pos = uv * u_model_size; float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius); - float border_alpha = (1.0 - step(0.0, out_distance)) * u_stroke_color.a; + float border_alpha = (1.0 - step(0.0, out_distance)); float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius); - float fill_alpha = (1.0 - step(0.0, in_distance)) * u_rectangle_color.a; + float fill_alpha = (1.0 - step(0.0, in_distance)); vec4 c1 = step(1-fill_alpha, 0) * u_rectangle_color; vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color; gl_FragColor = c1 + c2; @@ -47,9 +47,9 @@ init python: vec2 uv = v_tex_coord - vec2(0.5, 0.5); vec2 tex_pos = uv * u_model_size; float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius); - float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance)) * u_stroke_color.a; + float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance)); float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius); - float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance)) * u_rectangle_color.a; + float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance)); vec4 c1 = fill_alpha * u_rectangle_color; vec4 c2 = border_alpha * u_stroke_color; gl_FragColor = mix(c2, c1, fill_alpha); From 00f0ac67f4b96d4823d6df0c9bd8692e5ca835ee Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Thu, 13 Nov 2025 18:51:48 +0800 Subject: [PATCH 14/19] Add special screens (history, gallery) converting method. 1. Add special screens converting. 2. Support FairyFGUI sprite description format(7 fields every image) when publishing assets without cropping transparent parts. 3. Optimized list generating method. --- src/fgui_converter/FguiAssetsParseLib.py | 32 +- .../utils/renpy/Fgui2RenpyConverter.py | 317 +++++++++++++++--- 2 files changed, 296 insertions(+), 53 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index dfe8897..9a974a4 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -110,10 +110,11 @@ def TransStrToBoolean(str): class FguiSpriteInfo: """ FairyGUI资源包中的Sprite描述内容。 - 总计11个字段,包括image id、图集编号、在图集中的x坐标、在图集中的y坐标、image宽度、image高度、rotate、 - 原图相对image的x偏移、原图相对image的y偏移、原图宽度、原图高度。 + 支持7或11个字段: + - 7个字段:image id、图集编号、x、y、width、height、rotate + - 11个字段(完整):再加上offset_x、offset_y、source_width、source_height """ - def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x, offset_y, source_width, source_height): + def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x=0, offset_y=0, source_width=None, source_height=None): self.image_id = image_id self.atlas_index = int(atlas_index) self.x = int(x) @@ -123,8 +124,9 @@ def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x, self.rotate = int(rotate) self.offset_x = int(offset_x) self.offset_y = int(offset_y) - self.source_width = int(source_width) - self.source_height = int(source_height) + # 若缺省原图尺寸,则默认与裁切后尺寸一致 + self.source_width = int(source_width) if source_width is not None else int(width) + self.source_height = int(source_height) if source_height is not None else int(height) def __repr__(self): return f"FguiSpriteInfo({self.image_id}, {self.atlas_index}, {self.x}, {self.y}, {self.width}, {self.height}, {self.rotate}, {self.offset_x}, {self.offset_y}, {self.source_width}, {self.source_height})" @@ -134,10 +136,14 @@ def ParseFguiSpriteDescFile(sprite_desc_file): fgui_image_sets = [] with open(sprite_desc_file, "r", encoding="utf-8") as f: for line in f: + if line.startswith("//"): + continue parts = line.strip().split() - if len(parts) == 11: # 确保每行有11个字段 + if len(parts) == 11 or len(parts) == 7: # 支持7或11个字段 obj = FguiSpriteInfo(*parts) fgui_image_sets.append(obj) + else: + raise ValueError("Number of Sprite description's fields is neither 11 nor 7.") return fgui_image_sets # 从xml字符串创建lxml的etree对象 @@ -402,7 +408,7 @@ def hex_aarrggbb_to_rgba(hex_color): # 检查处理后的字符串长度是否为8或6 if hex_str_len != 8 and hex_str_len != 6: - raise ValueError("输入的十六进制字符串必须是8位(AARRGGBB)或6位(RRGGBB)") + raise ValueError("Color String must be 8 characters(AARRGGBB) or 6 characters(RRGGBB)") # 6位字符则加上alpha通道的默认值 ff if hex_str_len ==6: @@ -415,7 +421,7 @@ def hex_aarrggbb_to_rgba(hex_color): g = int(clean_hex[4:6], 16) b = int(clean_hex[6:8], 16) except ValueError: - raise ValueError("字符串包含无效的十六进制字符") + raise ValueError("Color Hex String contains invalid character.") return (r, g, b, a) @@ -753,8 +759,8 @@ def __init__(self, display_item_tree, package_description_id=None): margin = self.display_item_tree.get("margin") self.margin = tuple(map(int, margin.split(","))) if margin else None self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0") - self.line_item_count = int(self.display_item_tree.get("lineItemCount", "1")) - self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "1")) + self.line_item_count = int(self.display_item_tree.get("lineItemCount", "0")) + self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "0")) self.line_gap = int(self.display_item_tree.get("lineGap", "0")) self.col_gap = int(self.display_item_tree.get("colGap", "0")) self.default_item_url = self.display_item_tree.get("defaultItem") @@ -1069,16 +1075,16 @@ def clear(self): self.fgui_image_set.clear() self.fgui_atlas_dicts.clear() - def get_componentname_by_id(self, id): + def get_componentname_by_id(self, id : str) -> str: return self.package_desc.id_name_mapping[id] - def get_component_by_id(self, id): + def get_component_by_id(self, id : str) -> FguiComponent: for component in self.fgui_component_set: if component.id == id: return component return None - def get_image_size_by_id(self, id): + def get_image_size_by_id(self, id : str): for image in self.fgui_image_set: if image.image_id == id: return (image.width, image.height) diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index fc460d7..bb28c6a 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -6,7 +6,7 @@ import os import re import argparse - +import math import shutil from enum import IntEnum @@ -45,7 +45,7 @@ class FguiToRenpyConverter: default_background = '#fff' # 菜单类界面名称,需要添加 tag menu。 - menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about'] + menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about', 'gallery'] # 模态类界面名称,需要添加 Modal True。 modal_screen_name_list = ['confirm'] @@ -58,6 +58,10 @@ class FguiToRenpyConverter: # 存档相关界面 save_load_screen_name_list = ['save', 'load'] + + # 历史界面 + history_screen_name_list = ['history'] + history_item_screen_name_list = ['history_item'] def __init__(self, fgui_assets): self.fgui_assets = fgui_assets @@ -121,6 +125,9 @@ def generate_image_definitions(self): image_definitions.append("# 图像定义") image_definitions.append("# 从FairyGUI图集中提取的图像") + if not self.fgui_assets.fgui_image_set: + print("Image set is Null.") + for sprite in self.fgui_assets.fgui_image_set: # 找到对应的图像信息 image_info = None @@ -401,6 +408,14 @@ def is_say_screen(screen_name : str): def is_save_load_screen(screen_name : str): return screen_name in FguiToRenpyConverter.save_load_screen_name_list + @staticmethod + def is_history_screen(screen_name : str): + return screen_name in FguiToRenpyConverter.history_screen_name_list + + @staticmethod + def is_history_item(screen_name : str): + return screen_name in FguiToRenpyConverter.history_item_screen_name_list + def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1): screen_ui_code = [] # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}") @@ -528,6 +543,14 @@ def generate_screen(self, component : FguiComponent): self.generate_save_load_screen(component) return + # history_item和history界面的特殊处理 + if self.is_history_item(screen_name): + self.generate_history_item(component) + return + if self.is_history_screen(screen_name): + self.generate_history_screen(component) + return + # confirm 界面固定入参 if screen_name == 'confirm': screen_params = 'message, yes_action, no_action' @@ -755,6 +778,132 @@ def generate_save_load_screen(self, component : FguiComponent): self.screen_code.extend(self.screen_ui_code) return + def generate_history_screen(self, component :FguiComponent): + print("This is history screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + history_item = None + displayable_list_len = len(component.display_list.displayable_list) + history_list = None + history_list_index = -1 + + for i in range(displayable_list_len): + displayable = component.display_list.displayable_list[i] + # 搜索名为 save_slot_list 的列表组件 + if displayable.name == 'history_list' and isinstance(displayable, FguiList): + history_list = displayable + history_list_index = i + # 确认默认引用组件类型。 + history_item = self.fgui_assets.get_component_by_id(history_list.default_item_id) + break + if not history_list: + print(f"{component.name} contains no history list.") + return + # 检查history_list默认引用组件类型是否为component + if not isinstance(history_item, FguiComponent): + print(f"{component.name} history list item is not Component.") + return + + self.reset_indent_level() + self.screen_definition_head.append("# 对话历史界面") + self.screen_definition_head.append(f"screen {component.name}():") + self.indent_level_up() + self.screen_definition_head.append(f"{self.indent_str}tag menu") + self.screen_definition_head.append(f"{self.indent_str}predict False") + + # history_list 之前的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=history_list_index)) + + # history_list的处理 + history_list_code = [] + history_list_code.append(f"{self.indent_str}vpgrid:") + self.indent_level_up() + # 固定可拖拽 + history_list_code.append(f"{self.indent_str}draggable True") + # 固定一列 + history_list_code.append(f"{self.indent_str}cols 1") + history_list_code.append(f"{self.indent_str}yspacing {history_list.line_gap}") + history_list_code.append(f"{self.indent_str}pos {history_list.xypos}") + history_list_code.append(f"{self.indent_str}xysize {history_list.size}") + history_list_code.append(f"{self.indent_str}for h in _history_list:") + self.indent_level_up() + # item组件可能包含多个子组件,直接引用会出现vpgrid overfull错误 + history_list_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + history_list_code.append(f"{self.indent_str}xysize {history_item.size}") + history_list_code.append(f"{self.indent_str}use {history_item.name}(h.who, h.what)") + self.indent_level_down(3) + self.screen_ui_code.extend(history_list_code) + + # history_list 之后的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=history_list_index+1)) + self.screen_ui_code.append("") + self.reset_indent_level() + + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + return + + def generate_history_item(self, component : FguiComponent): + print("This is history item.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + who_text = None + what_text = None + namebox = None + textbox = None + namebox_pos = (0, 0) + textbox_pos = (0, 0) + + for displayable in component.display_list.displayable_list: + # 生成发言角色名的文本样式 + if displayable.name == 'who' and isinstance(displayable, FguiText): + self.generate_text_style(displayable, "history_who_text_style") + who_text = displayable + # 生成发言内容的文本样式 + if displayable.name == 'what' and isinstance(displayable, FguiText): + self.generate_text_style(displayable, "history_what_text_style") + what_text = displayable + # 检查查找结果 + if who_text == None: + print("Lack of who text component.") + return + if what_text == None: + print("Lack of what text component.") + return + + who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n") + what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n") + screen_params = f"who='{who_text_str}', what='{what_text_str}'" + self.reset_indent_level() + # say界面需要覆盖默认gui设置 + self.screen_definition_head.append("# history_item界面,用于显示一条对话记录。") + # self.screen_definition_head.append("style history_label is history_who_text_style") + # self.screen_definition_head.append("style history_dialogue is history_what_text_style") + self.screen_definition_head.append(f"screen {component.name}({screen_params}):") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}text _(who) style 'history_who_text_style':") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}") + self.indent_level_down() + self.screen_ui_code.append(f"{self.indent_str}text _(what) style 'history_what_text_style':") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}pos {what_text.xypos}") + self.reset_indent_level() + + self.screen_ui_code.append("") + self.screen_code.extend(self.screen_definition_head) + self.screen_code.extend(self.screen_ui_code) + + def generate_say_screen(self, component : FguiComponent): print("This is say screen.") self.screen_definition_head.clear() @@ -863,7 +1012,7 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100, def generate_button_parameter(button_title=None, original_actions_str=None, icon=None): parameter_str = "" title_str = "title=''" - actions_str = "actions=None" + actions_str = "actions=NullAction()" icon_str = "icon=Null()" if button_title : title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n") @@ -879,7 +1028,7 @@ def generate_button_parameter(button_title=None, original_actions_str=None, icon def generate_button_callable_parameter(button_title=None, actions=None, icon=None): parameter_str = "" title_str = "title=''" - actions_str = "actions=None" + actions_str = "actions=NullAction()" icon_str = "icon=Null()" if button_title : title_str = f"title={button_title}".replace("\n", "\\n").replace("\r", "\\n") @@ -1568,8 +1717,9 @@ def generate_list_displayable(self, fgui_list): # 若为组件 if default_item: default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id) - default_item_type = 'component' - # 若为图片 + # default_item_type = 'component' + default_item_type = default_item.extention + # 若非组件 else: default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id) if default_item_name: @@ -1578,43 +1728,110 @@ def generate_list_displayable(self, fgui_list): print("Ref com not found.") return list_code + # 列表与默认元素尺寸,可能用于grid或vpgrid的行数与列数计算 + list_xysize = fgui_list.size + default_item_size = default_item.size + list_length = len(fgui_list.item_list) + column_num = 1 + row_num = 1 + end_indent_level = 1 + # 根据“溢出处理”是否可见区分处理。 # 若“可见”,则使用hbox、vbox和grid。 if fgui_list.overflow == "visible": # 单列竖排,使用vbox if fgui_list.layout == "column": list_code.append(f"{self.indent_str}vbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}") + # 单行横排,使用hbox elif fgui_list.layout == "row": list_code.append(f"{self.indent_str}hbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}") + # 其他,使用grid else: - list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:") + # FguiList中的line_item_count和line_item_count2可能为0,需要根据列表尺寸与元素尺寸计算实际的行数与列数。 + if not (fgui_list.line_item_count and fgui_list.line_item_count2): + # 横向流动,填充行,之后换行 + if fgui_list.layout == 'flow_hz' : + column_num = math.floor(list_xysize[0] / default_item_size[0]) + row_num = math.ceil(list_length / column_num) + # 纵向流动,先填充列,之后换列 + elif fgui_list.layout == 'flow_vt' : + row_num = math.floor(list_xysize[1] / default_item_size[1]) + column_num = math.ceil(list_length / row_num) + list_code.append(f"{self.indent_str}grid {column_num} {row_num}:") + else: + list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:") + self.indent_level_up() + if fgui_list.layout == 'flow_vt' : + list_code.append(f"{self.indent_str}transpose True") + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}") + if fgui_list.col_gap: + list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}") + list_code.append(f"{self.indent_str}pos {fgui_list.xypos}") + list_code.append(f"{self.indent_str}xysize {fgui_list.size}") else: - list_code.append(f"{self.indent_str}vpgrid:") + end_indent_level = 2 + list_code.append(f"{self.indent_str}viewport:") self.indent_level_up() + list_code.append(f"{self.indent_str}pos {fgui_list.xypos}") + list_code.append(f"{self.indent_str}xysize {fgui_list.size}") + if fgui_list.margin: + list_code.append(f"{self.indent_str}margin {fgui_list.margin}") - # 若“隐藏”,使用不可滚动的vpgrid + # 若“隐藏”,使用不可滚动的viewport if fgui_list.overflow == "hidden": list_code.append(f"{self.indent_str}draggable False") - # 单列竖排 + list_code.append(f"{self.indent_str}mousewheel False") + # 单列竖排,使用vbox if fgui_list.layout == "column": - cols = 1 - rows = item_number - # 单行横排 + list_code.append(f"{self.indent_str}vbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}") + + # 单行横排,使用hbox elif fgui_list.layout == "row": - cols = item_number - rows = 1 - # 其他 + list_code.append(f"{self.indent_str}hbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}") + + # 其他,使用grid else: - cols = fgui_list.line_item_count - rows = fgui_list.line_item_count2 - list_code.append(f"{self.indent_str}cols {cols}") - list_code.append(f"{self.indent_str}rows {rows}") + # FguiList中的line_item_count和line_item_count2可能为0,需要根据列表尺寸与元素尺寸计算实际的行数与列数。 + if not (fgui_list.line_item_count and fgui_list.line_item_count2): + # 横向流动,填充行,之后换行 + if fgui_list.layout == 'flow_hz' : + column_num = math.floor(list_xysize[0] / default_item_size[0]) + row_num = math.ceil(list_length / column_num) + # 纵向流动,先填充列,之后换列 + elif fgui_list.layout == 'flow_vt' : + row_num = math.floor(list_xysize[1] / default_item_size[1]) + column_num = math.ceil(list_length / row_num) + list_code.append(f"{self.indent_str}grid {column_num} {row_num}:") + else: + list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:") + self.indent_level_up() + if fgui_list.layout == 'flow_vt' : + list_code.append(f"{self.indent_str}transpose True") + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}") + if fgui_list.col_gap: + list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}") + # 若“滚动”,使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。 elif fgui_list.overflow == "scroll": list_code.append(f"{self.indent_str}draggable True") + list_code.append(f"{self.indent_str}mousewheel True") # 垂直滚动 if fgui_list.scroll == "vertical": pass @@ -1626,43 +1843,63 @@ def generate_list_displayable(self, fgui_list): pass # 单列竖排,使用vbox if fgui_list.layout == "column": - cols = 1 - rows = item_number + list_code.append(f"{self.indent_str}vbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}") + # 单行横排,使用hbox elif fgui_list.layout == "row": - cols = item_number - rows = 1 + list_code.append(f"{self.indent_str}hbox:") + self.indent_level_up() + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}") + # 其他,使用grid else: - cols = fgui_list.line_item_count - rows = fgui_list.line_item_count2 - list_code.append(f"{self.indent_str}cols {cols}") - list_code.append(f"{self.indent_str}rows {rows}") - if fgui_list.line_gap: - list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}") - if fgui_list.col_gap: - list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}") - self.indent_level_down() + # FguiList中的line_item_count和line_item_count2可能为0,需要根据列表尺寸与元素尺寸计算实际的行数与列数。 + if not (fgui_list.line_item_count and fgui_list.line_item_count2): + # 横向流动,填充行,之后换行 + if fgui_list.layout == 'flow_hz' : + column_num = math.floor(list_xysize[0] / default_item_size[0]) + row_num = math.ceil(list_length / column_num) + # 纵向流动,先填充列,之后换列 + elif fgui_list.layout == 'flow_vt' : + row_num = math.floor(list_xysize[1] / default_item_size[1]) + column_num = math.ceil(list_length / row_num) + list_code.append(f"{self.indent_str}grid {column_num} {row_num}:") + else: + list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:") + self.indent_level_up() + if fgui_list.layout == 'flow_vt' : + list_code.append(f"{self.indent_str}transpose True") + if fgui_list.line_gap: + list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}") + if fgui_list.col_gap: + list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}") - self.indent_level_up() - list_code.append(f"{self.indent_str}pos {fgui_list.xypos}") - list_code.append(f"{self.indent_str}xysize {fgui_list.size}") - if fgui_list.margin: - list_code.append(f"{self.indent_str}margin {fgui_list.margin}") # 添加元素 for item in fgui_list.item_list: # 非默认元素 if item.item_url: + # TODO 非默认元素待处理 pass # 默认元素 else: if default_item_type == "image": list_code.append(f"{self.indent_str}add \'{default_item_name}\'") - elif default_item_type == "component": + elif default_item_type == "Button": parameter_str = self.generate_button_parameter(item.item_title) list_code.append(f"{self.indent_str}use {default_item_name}({parameter_str})") + else: + # 非按钮组件可能包含多个子组件,直接引用会出现vpgrid overfull错误 + list_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + list_code.append(f"{self.indent_str}xysize {default_item.size}") + list_code.append(f"{self.indent_str}use {default_item_name}()") + self.indent_level_down() - self.indent_level_down() + self.indent_level_down(end_indent_level) return list_code def generate_renpy_code(self): From 96f9289ec154c9d2884191d0b29a58db9e9a7d7a Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Fri, 14 Nov 2025 00:41:47 +0800 Subject: [PATCH 15/19] Children of screen could be shown or hide by Fgui controllers. --- src/fgui_converter/FguiAssetsParseLib.py | 11 +++- .../utils/renpy/Fgui2RenpyConverter.py | 59 ++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index 9a974a4..506f797 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -580,7 +580,8 @@ def __init__(self, component_property_tree): class FguiButtonProperty(FguiComponentPropertyBase): """ 组件子属性中的Button信息。 - 通常包括title和icon,分别表示标题(文本)和图标(装载器) + 包括title和icon,分别表示标题(文本)和图标(装载器)。 + 若按钮与控制器连接,在此处记录关联控制器名与关联索引。 """ def __init__(self, component_property_tree): super().__init__(component_property_tree) @@ -590,7 +591,10 @@ def __init__(self, component_property_tree): self.selected_title = self.component_property_tree.get("selectedTitle") self.icon = self.component_property_tree.get("icon") self.selected_icon = self.component_property_tree.get("selectedIcon") - + self.checked = self.component_property_tree.get("checked", False) + self.controller_name = self.component_property_tree.get("controller") + self.controller_index = self.component_property_tree.get("page") + # TODO 待添加点击音效 class FguiGraph(FguiDisplayable): """ @@ -841,7 +845,8 @@ def __init__(self, gear_item_tree): controller_index = gear_item_tree.get("pages") values = gear_item_tree.get("values") if controller_index: - self.controller_index = controller_index.split(",") + self.controller_index = [int(i) for i in controller_index.split(",")] + print(self.controller_index) self.values = None if values: self.values = values.split("|") diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index bb28c6a..5174ed0 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -439,6 +439,15 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in pass # 其他组件 else: + end_indent_level = 1 + + # 根据显示控制器gearDisplay设置显示条件 + if displayable.gear_display: + condition_str = f"showif {displayable.gear_display.controller_name} in {displayable.gear_display.controller_index}:" + screen_ui_code.append(f"{self.indent_str}{condition_str}") + self.indent_level_up() + end_indent_level = 2 + # 根据引用源id查找组件 ref_com = self.fgui_assets.get_component_by_id(displayable.src) # 按钮。可设置标题,并根据自定义数据字段设置action。 @@ -448,13 +457,20 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。 actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data + action_list = [] + if actions: + action_list.append(actions) # 此处仅处理了title,而未处理selected_title。后续可能需要添加。 if displayable.button_property: + if displayable.button_property.controller_name: + button_controller_action = f"SetScreenVariable('{displayable.button_property.controller_name}', {displayable.button_property.controller_index})" + action_list.append(button_controller_action) + actions = f"[{', '.join(action_list)}]" parameter_str = self.generate_button_parameter(displayable.button_property.title, actions) else: parameter_str = self.generate_button_parameter(None, actions) screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{component.name}_{displayable.id}'") - self.indent_level_down() + self.indent_level_down(end_indent_level) continue # 滑动条 if ref_com.extention == "Slider" and ref_com.name != None: @@ -473,16 +489,15 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}") bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value) screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'") - self.indent_level_down() + self.indent_level_down(end_indent_level) continue # 其他组件 screen_ui_code.append(f"{self.indent_str}fixed:") self.indent_level_up() screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}") screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'") - self.indent_level_down() - # print("convert_component_display_list method:") - # print(screen_ui_code) + self.indent_level_down(end_indent_level) + return screen_ui_code def generate_screen(self, component : FguiComponent): @@ -566,6 +581,16 @@ def generate_screen(self, component : FguiComponent): if self.is_modal_screen(screen_name): self.screen_ui_code.append(f"{self.indent_str}modal True") self.screen_ui_code.append(f"{self.indent_str}zorder 200\n") + + # 根据控制器列表定义界面内变量 + if component.controller_list: + self.screen_variable_code.append(f"{self.indent_str}# 由组件控制器生成的界面内控制变量:") + for controller in component.controller_list: + if not isinstance(controller, FguiController): + print("Component controller object type is wrong.") + break + self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}") + self.screen_ui_code.extend(self.convert_component_display_list(component)) self.screen_code.extend(self.screen_definition_head) @@ -1283,7 +1308,7 @@ def generate_button_screen(self, component): # 其他状态根据枚举值加入各列表 else: for i in range(0, state_number): - if str(i) in displayable.gear_display.controller_index: + if i in displayable.gear_display.controller_index: state_children_dict[state_index_name_dict[i]].append((displayable, FguiToRenpyConverter.get_child_type(displayable))) continue # 图片组件 @@ -1488,6 +1513,15 @@ def generate_image_displayable(self, fgui_image : FguiImage): print("It is not a image displayable.") return image_code + end_indent_level = 0 + + # 根据显示控制器gearDisplay设置显示条件 + if fgui_image.gear_display: + condition_str = f"showif {fgui_image.gear_display.controller_name} in {fgui_image.gear_display.controller_index}:" + image_code.append(f"{self.indent_str}{condition_str}") + self.indent_level_up() + end_indent_level = 1 + for image in self.fgui_assets.package_desc.image_list: if fgui_image.src == image.id: image_name = image.name @@ -1525,6 +1559,7 @@ def generate_image_displayable(self, fgui_image : FguiImage): image_code.append(f"{self.indent_str}xysize {size}") self.indent_level_down() break + self.indent_level_down(end_indent_level) return image_code def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : str) -> list: @@ -1559,6 +1594,16 @@ def generate_text_displayable(self, fgui_text): print("It is not a text displayable.") return text_code + + end_indent_level = 1 + + # 根据显示控制器gearDisplay设置显示条件 + if fgui_text.gear_display: + condition_str = f"showif {fgui_text.gear_display.controller_name} in {fgui_text.gear_display.controller_index}:" + text_code.append(f"{self.indent_str}{condition_str}") + self.indent_level_up() + end_indent_level = 2 + # 直接定义text组件。 # 处理换行符 text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n") @@ -1697,7 +1742,7 @@ def generate_text_displayable(self, fgui_text): if fgui_text.is_input: self.indent_level_down() - self.indent_level_down() + self.indent_level_down(end_indent_level) return text_code def generate_list_displayable(self, fgui_list): From 1b3e15f9a813e4a12d8afc95008e63ec3437f071 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Fri, 14 Nov 2025 11:09:11 +0800 Subject: [PATCH 16/19] Fix issue of save/load screen lack of tag menu. --- src/fgui_converter/FguiAssetsParseLib.py | 1 - src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index 506f797..de0e7d5 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -846,7 +846,6 @@ def __init__(self, gear_item_tree): values = gear_item_tree.get("values") if controller_index: self.controller_index = [int(i) for i in controller_index.split(",")] - print(self.controller_index) self.values = None if values: self.values = values.split("|") diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 5174ed0..142f4f9 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -708,6 +708,9 @@ def generate_save_load_screen(self, component : FguiComponent): self.screen_definition_head.append("# 存档/读档 界面") self.screen_definition_head.append(f"screen {component.name}():") + + # 菜单标签 + self.screen_definition_head.append(f"{self.indent_str}tag menu") # save_slot_list 之前的组件 self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=slot_list_index)) From dc54c125cb717cf0d6d590675a6330863f715fd3 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Mon, 8 Dec 2025 18:19:49 +0800 Subject: [PATCH 17/19] Implementation hidden(part display) and scrollable with screen level. --- src/fgui_converter/FguiAssetsParseLib.py | 9 ++++ .../utils/renpy/Fgui2RenpyConverter.py | 46 +++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index de0e7d5..b2353ab 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -261,6 +261,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None): self.package_desc_id = package_desc_id size = component_etree.get("size") self.size = tuple(map(int, size.split(","))) if size else (0,0) + self.overflow = component_etree.get("overflow", "visible") + self.scroll = component_etree.get("scroll", "vertical") self.extention = component_etree.get("extention") self.mask = component_etree.get("mask") # 轴心。默认值为(0.0, 0.0)。 @@ -272,6 +274,10 @@ def __init__(self, component_etree, id, name, package_desc_id=None): # 自定义数据。实际使用时,相同id的FguiDisplayable中的自定义数据优先。 self.custom_data = component_etree.get("customData") + #包含所有子组件的包围框尺寸,初始值为组件尺寸。 + self.bbox_width = self.size[0] + self.bbox_height = self.size[1] + # 控制器,一般不超过1个。 self.controller_list = [] # 显示内容列表,通常为image、text、graph。 @@ -286,6 +292,9 @@ def __init__(self, component_etree, id, name, package_desc_id=None): # 显示内容 elif (self.component_etree[i].tag == "displayList"): self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id) + for displayable in self.display_list.displayable_list: + self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable.size[0]) + self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable.size[1]) def __repr__(self): return f"FguiComponent({self.id}, {self.name}, {self.size}, {self.extention}, {self.mask})" diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 142f4f9..ab4514c 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -591,6 +591,22 @@ def generate_screen(self, component : FguiComponent): break self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}") + # 根据组件的可见区域性质,决定是否加一层viewport。 + if component.overflow == "hidden": + self.screen_ui_code.append(f"{self.indent_str}viewport:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}") + elif component.overflow == "scroll": + self.screen_ui_code.append(f"{self.indent_str}viewport:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}") + self.screen_ui_code.append(f"{self.indent_str}draggable True") + # 在Ren'Py中实际可能无法滚动。 + # 需要添加一个fixed组件,并设置一个合适的xysize。该xysize应为容纳所有子组件的包围框。 + self.screen_ui_code.append(f"{self.indent_str}fixed:") + self.indent_level_up() + self.screen_ui_code.append(f"{self.indent_str}xysize ({component.bbox_width}, {component.bbox_height})") + self.screen_ui_code.extend(self.convert_component_display_list(component)) self.screen_code.extend(self.screen_definition_head) @@ -890,16 +906,23 @@ def generate_history_item(self, component : FguiComponent): textbox = None namebox_pos = (0, 0) textbox_pos = (0, 0) + who_index = 0 + what_index = 0 - for displayable in component.display_list.displayable_list: + display_list_len = len(component.display_list.displayable_list) + for i in range(display_list_len): + displayable = component.display_list.displayable_list[i] # 生成发言角色名的文本样式 if displayable.name == 'who' and isinstance(displayable, FguiText): self.generate_text_style(displayable, "history_who_text_style") who_text = displayable + who_index = i # 生成发言内容的文本样式 if displayable.name == 'what' and isinstance(displayable, FguiText): self.generate_text_style(displayable, "history_what_text_style") what_text = displayable + what_index = i + # 检查查找结果 if who_text == None: print("Lack of who text component.") @@ -908,8 +931,10 @@ def generate_history_item(self, component : FguiComponent): print("Lack of what text component.") return - who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n") - what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n") + # who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n") + # what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n") + who_text_str = '' + what_text_str = '无对话记录。' screen_params = f"who='{who_text_str}', what='{what_text_str}'" self.reset_indent_level() # say界面需要覆盖默认gui设置 @@ -918,6 +943,15 @@ def generate_history_item(self, component : FguiComponent): # self.screen_definition_head.append("style history_dialogue is history_what_text_style") self.screen_definition_head.append(f"screen {component.name}({screen_params}):") self.indent_level_up() + + # 不包含who和what的displayList部分按顺序放在前面 + index_min = min(what_index, who_index) + index_max = max(what_index, who_index) + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=index_min)) + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=index_min+1, list_end_index=index_max)) + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=index_max+1)) + + # who和what固定放在后面,显示在其他组件之上。 self.screen_ui_code.append(f"{self.indent_str}text _(who) style 'history_who_text_style':") self.indent_level_up() self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}") @@ -953,6 +987,7 @@ def generate_say_screen(self, component : FguiComponent): # 生成发言角色名的文本样式 if displayable.name == 'who' and isinstance(displayable, FguiText): self.generate_text_style(displayable, "say_who_text_style") + self.generate_text_style(displayable, "say_label") who_text = displayable # 生成发言内容的文本样式 if displayable.name == 'what' and isinstance(displayable, FguiText): @@ -1092,6 +1127,7 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str): self.style_code.append(f"# 文本{fgui_text.name}样式定义") # 定义样式 self.style_code.append(f"style {style_name}:") + self.style_code.append(f"{style_indent}anchor {fgui_text.pivot}") self.style_code.append(f"{style_indent}xysize {fgui_text.size}") # 字体可能为空,改为Ren'Py内置默认字体SourceHanSansLite text_font = fgui_text.font if fgui_text.font else "SourceHanSansLite" @@ -1721,6 +1757,10 @@ def generate_text_displayable(self, fgui_text): # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置 xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align) text_code.append(f"{self.indent_str}textalign {xalign}") + # 不自动换行 + text_code.append(f"{self.indent_str}layout 'nobreak'") + + # 粗体、斜体、下划线、删除线 if fgui_text.bold: text_code.append(f"{self.indent_str}bold {fgui_text.bold}") From a24f709e0882a53a3ae7d8676bad7676789a3902 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Tue, 6 Jan 2026 18:31:37 +0800 Subject: [PATCH 18/19] Implementation of screen "gallery". 1. Add "gallery" template. 2. "gallery" component is marked special screen, with specific converting method. --- src/fgui_converter/FguiAssetsParseLib.py | 1 + .../utils/renpy/Fgui2RenpyConverter.py | 208 +++++++++++++++++- .../renpy_gallery_template.txt | 30 +++ 3 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py index b2353ab..c03224f 100644 --- a/src/fgui_converter/FguiAssetsParseLib.py +++ b/src/fgui_converter/FguiAssetsParseLib.py @@ -293,6 +293,7 @@ def __init__(self, component_etree, id, name, package_desc_id=None): elif (self.component_etree[i].tag == "displayList"): self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id) for displayable in self.display_list.displayable_list: + # 包围框尺寸。FGUI不考虑子组件坐标xy小于0的情况,仅扩展组件的右侧和下方。 self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable.size[0]) self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable.size[1]) diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index ab4514c..19a5cf0 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -45,7 +45,7 @@ class FguiToRenpyConverter: default_background = '#fff' # 菜单类界面名称,需要添加 tag menu。 - menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about', 'gallery'] + menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about'] # 模态类界面名称,需要添加 Modal True。 modal_screen_name_list = ['confirm'] @@ -63,6 +63,12 @@ class FguiToRenpyConverter: history_screen_name_list = ['history'] history_item_screen_name_list = ['history_item'] + # 图鉴类界面 + gallery_screen_name_list = ['gallery', 'music_room'] + + # 所以特殊界面名 + special_screen_name_list = [] + def __init__(self, fgui_assets): self.fgui_assets = fgui_assets self.renpy_code = [] @@ -75,6 +81,11 @@ def __init__(self, fgui_assets): self.image_definition_code = [] self.graph_definition_code = [] self.game_global_variables_code = [] + self.gallery_screen_code = [] + self.music_room_screen_code = [] + + # 所有特殊界面名 + self.special_screen_name_list = self.menu_screen_name_list + self.modal_screen_name_list + self.choice_screen_name_list + self.say_screen_name_list + self.save_load_screen_name_list + self.history_screen_name_list + self.gallery_screen_name_list # dismiss用于取消一些组件的focus状态,例如input。 self.screen_has_dismiss = False @@ -94,9 +105,15 @@ def __init__(self, fgui_assets): os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates")) self.font_map_template = 'renpy_font_map_definition.txt' self.graph_template_dict = {} - self.graph_template_dict['null'] = self.get_graph_template('renpy_null_template.txt') - self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt') - self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt') + self.graph_template_dict['null'] = self.get_template_content('renpy_null_template.txt') + self.graph_template_dict['rectangle'] = self.get_template_content('renpy_rectangle_template.txt') + self.graph_template_dict['ellipse'] = self.get_template_content('renpy_ellipse_template.txt') + self.gallery_template_dict = {} + self.gallery_template_dict['gallery'] = self.get_template_content('renpy_gallery_template.txt') + self.gallery_template_dict['music_room'] = self.get_template_content('renpy_music_room_template.txt') + + # 输出游戏目录 + self.game_dir = None def set_game_global_variables(self, variable_name, variable_value): variable_str = f"define {variable_name} = {variable_value}" @@ -416,6 +433,14 @@ def is_history_screen(screen_name : str): def is_history_item(screen_name : str): return screen_name in FguiToRenpyConverter.history_item_screen_name_list + @staticmethod + def is_gallery_screen(screen_name : str): + return screen_name == 'gallery' + + @staticmethod + def is_music_room_screen(screen_name : str): + return screen_name == 'music_room' + def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1): screen_ui_code = [] # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}") @@ -543,6 +568,7 @@ def generate_screen(self, component : FguiComponent): # 界面入参列表 screen_params = '' + # choice界面的特殊处理 if self.is_choice_screen(screen_name): self.generate_choice_screen(component) @@ -566,6 +592,14 @@ def generate_screen(self, component : FguiComponent): self.generate_history_screen(component) return + # gallery和music_room界面的特殊处理 + if self.is_gallery_screen(screen_name): + self.generate_gallery_screen(component) + return + if self.is_music_room_screen(screen_name): + self.generate_music_room_screen(component) + return + # confirm 界面固定入参 if screen_name == 'confirm': screen_params = 'message, yes_action, no_action' @@ -965,6 +999,137 @@ def generate_history_item(self, component : FguiComponent): self.screen_code.extend(self.screen_definition_head) self.screen_code.extend(self.screen_ui_code) + def generate_gallery_screen(self, component : FguiComponent): + print("This is gallery screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + self.gallery_screen_code.clear() + + if not self.gallery_template_dict['gallery']: + print("Gallery template is empty.") + return + + gallery_button_list = None + gallery_button_list_item = None + gallery_button_list_index = -1 + displayable_list_len = len(component.display_list.displayable_list) + gallery_button_list_len = 1 + gallery_button_list_column = 1 + gallery_button_list_row = 1 + ui_helper_exists = False + # 从game目录中 01_ui_helper.rpy 文件中获取实际的 gallery_image_list。 + # 此处只检查文件是否存在,不检查文件内容。 + gallery_image_list_file = os.path.join(self.game_dir, '01_ui_helper.rpy') + if not os.path.exists(gallery_image_list_file): + print(f"Gallery image list file {gallery_image_list_file} does not exist.") + else: + ui_helper_exists = True + + for i in range(displayable_list_len): + displayable = component.display_list.displayable_list[i] + # 搜索名为 gallery_button_list 的列表组件 + if displayable.name == 'gallery_button_list' and isinstance(displayable, FguiList): + gallery_button_list = displayable + # 确认默认引用组件类型。 + gallery_button_list_item = self.fgui_assets.get_component_by_id(gallery_button_list.default_item_id) + gallery_button_list_index = i + break + if not gallery_button_list: + print(f"{component.name} contains no gallery button list.") + return + # 检查gallery_button_list_item是否为按钮 + if not isinstance(gallery_button_list_item, FguiButton): + print(f"{component.name} gallery button list item is not Button.") + return + + # 计算列表每行显示的按钮数量 + gallery_button_list_len = max(len(displayable.item_list), 1) + gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0]) + gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column) + + self.reset_indent_level() + self.screen_definition_head.append("# CG图鉴界面") + self.screen_definition_head.append(f"screen {component.name}():") + self.indent_level_up() + self.screen_definition_head.append(f"{self.indent_str}tag menu") + self.screen_definition_head.append(f"{self.indent_str}") + + # gallery_button_list 之前的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=gallery_button_list_index)) + + # gallery_button_list 的处理 + gallery_button_list_code = [] + gallery_button_list_code.append(f"{self.indent_str}viewport:") + self.indent_level_up() + gallery_button_list_code.append(f"{self.indent_str}pos {gallery_button_list.xypos}") + gallery_button_list_code.append(f"{self.indent_str}xysize {gallery_button_list.size}") + # 固定可拖拽 + gallery_button_list_code.append(f"{self.indent_str}draggable True") + gallery_button_list_code.append(f"{self.indent_str}mousewheel True") + gallery_button_list_code.append(f"{self.indent_str}grid gallery_view_column gallery_view_row:") + self.indent_level_up() + gallery_button_list_code.append(f"{self.indent_str}yspacing {gallery_button_list.line_gap}") + gallery_button_list_code.append(f"{self.indent_str}xspacing {gallery_button_list.col_gap}") + # 若ui_helper.rpy不存在,则根据列表长度添加对应数量的默认按钮 + if not ui_helper_exists: + for i in range(len(gallery_button_list.item_list)): + gallery_button_list_code.append(f"{self.indent_str}use {gallery_button_list_item.name}()") + # 若ui_helper.rpy存在,则使用固定脚本定义的按钮 + else: + gallery_button_list_code.append(f"{self.indent_str}for i in range(gallery_button_num):") + self.indent_level_up() + gallery_button_list_code.append(f"{self.indent_str}python:") + self.indent_level_up() + gallery_button_list_code.append(f"{self.indent_str}gallery_image = gallery_image_list[i]") + gallery_button_list_code.append(f"{self.indent_str}if isinstance(gallery_image, list):") + self.indent_level_up() + gallery_button_list_code.append(f"{self.indent_str}gallery_image = gallery_image[0]") + self.indent_level_down(2) + # 暂时用按钮的第一个display作为locked状态的显示内容。 + locked_image = self.fgui_assets.get_componentname_by_id(gallery_button_list_item.display_list.displayable_list[0].src) + gallery_button_list_code.append(f"{self.indent_str}add g.make_button(gallery_image, gallery_image, locked='{locked_image}') xysize {gallery_button_list_item.size}") + self.indent_level_down() + self.screen_ui_code.extend(gallery_button_list_code) + + self.indent_level_down(2) + # gallery_button_list 之后的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=gallery_button_list_index+1)) + self.screen_ui_code.append("") + self.reset_indent_level() + + self.gallery_screen_code.extend(self.screen_definition_head) + self.gallery_screen_code.extend(self.screen_ui_code) + # 读取 templates/renpy_gallery_template.txt 模板内容,并替换 {gallery_screen_code} 为 self.gallery_screen_code + gallery_template_content = self.get_template_content('renpy_gallery_template.txt') + gallery_template_content = gallery_template_content.replace('{gallery_screen_code}', '\n'.join(self.gallery_screen_code)) + gallery_template_content = gallery_template_content.replace('{gallery_button_list_len}', str(gallery_button_list_len)) + gallery_template_content = gallery_template_content.replace('{gallery_button_list_column}', str(gallery_button_list_column)) + gallery_template_content = gallery_template_content.replace('{gallery_button_list_row}', str(gallery_button_list_row)) + self.gallery_screen_code = gallery_template_content.split('\n') + + + # 将内容保存在 game/scripts/gallery_screen.rpy 文件中 + gallery_screen_file = os.path.join(self.game_dir, 'scripts', 'gallery_screen.rpy') + self.save_code_to_file(gallery_screen_file, self.gallery_screen_code) + print(f"Gallery screen code has been saved to {gallery_screen_file}.") + return + + def generate_music_room_screen(self, component : FguiComponent): + print("This is music room screen.") + self.screen_definition_head.clear() + self.screen_variable_code.clear() + self.screen_function_code.clear() + self.screen_ui_code.clear() + self.screen_has_dismiss = False + self.dismiss_action_list.clear() + + if not self.gallery_template_dict['music_room']: + print("Music room template is empty.") + return def generate_say_screen(self, component : FguiComponent): print("This is say screen.") @@ -2037,6 +2202,16 @@ def save_to_file(self, filename): print(f"Ren'Py代码已保存到: {filename}") + def save_code_to_file(self, filename : str, code : list[str]) -> None: + """ + 保存Ren'Py代码 + """ + with open(filename, 'w', encoding='utf-8') as f: + for line in code: + f.write(line + '\n') + + print(f"Ren'Py代码已保存到: {filename}") + def from_templates_to_renpy(self, filename): """ 读取模板替换字符串并保存至Ren'Py目录 @@ -2050,11 +2225,13 @@ def from_templates_to_renpy(self, filename): with open(filename, 'w', encoding='utf-8') as f: f.write(content) - def get_graph_template(self, filename): + def get_template_content(self, filename : str, path : str = None) -> str: """ - 获取graph对应的image对象定义模板 + 获取模板内容 """ - with open(os.path.join(self.renpy_template_dir, filename), 'r', encoding='utf-8') as file: + template_path = self.renpy_template_dir if path is None else path + + with open(os.path.join(template_path, filename), 'r', encoding='utf-8') as file: content = file.read() return content @@ -2200,6 +2377,7 @@ def convert(argv): # 创建转换器 print("正在创建转换器...") converter = FguiToRenpyConverter(fgui_assets) + converter.game_dir = game_dir print("转换器创建完成") # 生成Ren'Py代码 @@ -2208,8 +2386,20 @@ def convert(argv): print("Ren'Py代码生成完成") # 保存.rpy文件到game目录 - output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy") - converter.save_to_file(output_file) + # output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy") + global_variables_output_file = os.path.join(scripts_dir, "preppipe_global_variables.rpy") + image_definition_output_file = os.path.join(scripts_dir, "preppipe_image_definition.rpy") + style_output_file = os.path.join(scripts_dir, "preppipe_styles.rpy") + screen_output_file = os.path.join(scripts_dir, "preppipe_screens.rpy") + # converter.save_to_file(output_file) + # self.renpy_code.extend(self.game_global_variables_code) + # self.renpy_code.extend(self.image_definition_code) + # self.renpy_code.extend(self.graph_definition_code) + # self.renpy_code.extend(self.style_code) + converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code) + converter.save_code_to_file(image_definition_output_file, converter.image_definition_code) + converter.save_code_to_file(style_output_file, converter.style_code) + converter.save_code_to_file(screen_output_file, converter.screen_code) # 部分预定义模板文件修改参数并保存 font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy") diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt new file mode 100644 index 0000000..f19f179 --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt @@ -0,0 +1,30 @@ +transform gallery_full_screen: + xpos 0 + ypos 0 + xsize 1.0 + ysize 1.0 + +init python: + g = Gallery() + for e in gallery_image_list: + if isinstance(e, list): + # CG组有多个图片 + # 使用第一个图片的名称 + g.button(e[0]) + for img in e: + g.image(img) + g.unlock(img) + g.transform(gallery_full_screen) + else: + # 单个图片 + g.button(e) + g.image(e) + g.unlock(e) + g.transform(gallery_full_screen) + # gallery_button_num = len(gallery_image_list) + gallery_button_num = {gallery_button_list_len} + g.transition = None + gallery_view_column = {gallery_button_list_column} + gallery_view_row = {gallery_button_list_row} + +{gallery_screen_code} \ No newline at end of file From dfe49e269d54b21197e8826c18c733067a534047 Mon Sep 17 00:00:00 2001 From: CursedOctopus Date: Sun, 11 Jan 2026 01:08:26 +0800 Subject: [PATCH 19/19] Add music_room screen implementation Add music_room screen. --- .../utils/renpy/Fgui2RenpyConverter.py | 231 ++++++++++++++++-- .../renpy_gallery_template.txt | 1 - .../renpy_music_room_template.txt | 29 +++ 3 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py index 19a5cf0..10f0e6a 100644 --- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py +++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py @@ -8,6 +8,7 @@ import argparse import math import shutil +import ast from enum import IntEnum # 复合型组件的子组件枚举类型 @@ -69,6 +70,9 @@ class FguiToRenpyConverter: # 所以特殊界面名 special_screen_name_list = [] + # 主管线提供的gallery数据文件 + ui_helper_file_name = '01_ui_helper.rpy' + def __init__(self, fgui_assets): self.fgui_assets = fgui_assets self.renpy_code = [] @@ -115,6 +119,9 @@ def __init__(self, fgui_assets): # 输出游戏目录 self.game_dir = None + # gallery相关数据 + self.gallery_data = {} + def set_game_global_variables(self, variable_name, variable_value): variable_str = f"define {variable_name} = {variable_value}" self.game_global_variables_code.append(variable_str) @@ -441,7 +448,7 @@ def is_gallery_screen(screen_name : str): def is_music_room_screen(screen_name : str): return screen_name == 'music_room' - def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1): + def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1) -> list: screen_ui_code = [] # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}") end_index = len(component.display_list.displayable_list) if (list_end_index == -1) else list_end_index @@ -1021,13 +1028,11 @@ def generate_gallery_screen(self, component : FguiComponent): gallery_button_list_column = 1 gallery_button_list_row = 1 ui_helper_exists = False - # 从game目录中 01_ui_helper.rpy 文件中获取实际的 gallery_image_list。 - # 此处只检查文件是否存在,不检查文件内容。 - gallery_image_list_file = os.path.join(self.game_dir, '01_ui_helper.rpy') - if not os.path.exists(gallery_image_list_file): - print(f"Gallery image list file {gallery_image_list_file} does not exist.") - else: + # 检查 gallery_data 是否存在gallery_image_list + if 'gallery_image_list' in self.gallery_data: ui_helper_exists = True + else: + print(f"Gallery image list is not defined in {self.ui_helper_file_name}.") for i in range(displayable_list_len): displayable = component.display_list.displayable_list[i] @@ -1046,10 +1051,24 @@ def generate_gallery_screen(self, component : FguiComponent): print(f"{component.name} gallery button list item is not Button.") return - # 计算列表每行显示的按钮数量 - gallery_button_list_len = max(len(displayable.item_list), 1) - gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0]) - gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column) + # 计算列表的行数与列数 + if ui_helper_exists: + gallery_button_list_len = len(self.gallery_data['gallery_image_list']) + else: + gallery_button_list_len = max(len(displayable.item_list), 1) + # FguiList中的line_item_count和line_item_count2可能为0,需要根据列表尺寸与元素尺寸计算实际的行数与列数。 + if not (gallery_button_list.line_item_count and gallery_button_list.line_item_count2): + # 横向流动,填充行,之后换行 + if gallery_button_list.layout == 'flow_hz' : + gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0]) + gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column) + # 纵向流动,先填充列,之后换列 + elif gallery_button_list.layout == 'flow_vt' : + gallery_button_list_row = math.floor(gallery_button_list.size[1] / gallery_button_list_item.size[1]) + gallery_button_list_column = math.ceil(gallery_button_list_len / gallery_button_list_row) + else: + gallery_button_list_column = gallery_button_list.line_item_count + gallery_button_list_row = gallery_button_list.line_item_count2 self.reset_indent_level() self.screen_definition_head.append("# CG图鉴界面") @@ -1070,10 +1089,14 @@ def generate_gallery_screen(self, component : FguiComponent): # 固定可拖拽 gallery_button_list_code.append(f"{self.indent_str}draggable True") gallery_button_list_code.append(f"{self.indent_str}mousewheel True") + # 鼠标放在viewport边缘自动滚动 + gallery_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)") gallery_button_list_code.append(f"{self.indent_str}grid gallery_view_column gallery_view_row:") self.indent_level_up() gallery_button_list_code.append(f"{self.indent_str}yspacing {gallery_button_list.line_gap}") gallery_button_list_code.append(f"{self.indent_str}xspacing {gallery_button_list.col_gap}") + if gallery_button_list.layout == 'flow_vt' : + gallery_button_list_code.append(f"{self.indent_str}transpose True") # 若ui_helper.rpy不存在,则根据列表长度添加对应数量的默认按钮 if not ui_helper_exists: for i in range(len(gallery_button_list.item_list)): @@ -1126,11 +1149,120 @@ def generate_music_room_screen(self, component : FguiComponent): self.screen_ui_code.clear() self.screen_has_dismiss = False self.dismiss_action_list.clear() + self.music_room_screen_code.clear() + + musicroom_button_list = None + musicroom_button_list_item = None + musicroom_button_list_index = -1 + displayable_list_len = len(component.display_list.displayable_list) + musicroom_button_list_len = 1 + musicroom_button_list_column = 1 + musicroom_button_list_row = 1 + ui_helper_exists = False + # 检查 gallery_data 是否存在gallery_image_list + if 'gallery_music_list' in self.gallery_data: + ui_helper_exists = True + else: + print(f"Gallery music list is not defined in {self.ui_helper_file_name}.") - if not self.gallery_template_dict['music_room']: - print("Music room template is empty.") + for i in range(displayable_list_len): + displayable = component.display_list.displayable_list[i] + # 搜索名为 musicroom_button_list 的列表组件 + if displayable.name == 'musicroom_button_list' and isinstance(displayable, FguiList): + musicroom_button_list = displayable + # 确认默认引用组件类型。 + musicroom_button_list_item = self.fgui_assets.get_component_by_id(musicroom_button_list.default_item_id) + musicroom_button_list_index = i + break + if not musicroom_button_list: + print(f"{component.name} contains no musicroom button list.") + return + # 检查musicroom_button_list_item是否为按钮 + if not isinstance(musicroom_button_list_item, FguiButton): + print(f"{component.name} musicroom button list item is not Button.") return + # 计算列表的行数与列数 + if ui_helper_exists: + musicroom_button_list_len = len(self.gallery_data['gallery_music_list']) + else: + musicroom_button_list_len = max(len(displayable.item_list), 1) + # FguiList中的line_item_count和line_item_count2可能为0,需要根据列表尺寸与元素尺寸计算实际的行数与列数。 + if not (musicroom_button_list.line_item_count and musicroom_button_list.line_item_count2): + # 横向流动,填充行,之后换行 + if musicroom_button_list.layout == 'flow_hz' : + musicroom_button_list_column = math.floor(musicroom_button_list.size[0] / musicroom_button_list_item.size[0]) + musicroom_button_list_row = math.ceil(musicroom_button_list_len / musicroom_button_list_column) + # 纵向流动,先填充列,之后换列 + elif musicroom_button_list.layout == 'flow_vt' : + musicroom_button_list_row = math.floor(musicroom_button_list.size[1] / musicroom_button_list_item.size[1]) + musicroom_button_list_column = math.ceil(musicroom_button_list_len / musicroom_button_list_row) + else: + musicroom_button_list_column = musicroom_button_list.line_item_count + musicroom_button_list_row = musicroom_button_list.line_item_count2 + + self.reset_indent_level() + self.screen_definition_head.append("# 音乐室界面") + self.screen_definition_head.append(f"screen {component.name}():") + self.indent_level_up() + self.screen_definition_head.append(f"{self.indent_str}tag menu") + self.screen_definition_head.append(f"{self.indent_str}") + + # musicroom_button_list 之前的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=musicroom_button_list_index)) + + # musicroom_button_list 的处理 + musicroom_button_list_code = [] + musicroom_button_list_code.append(f"{self.indent_str}viewport:") + self.indent_level_up() + musicroom_button_list_code.append(f"{self.indent_str}pos {musicroom_button_list.xypos}") + musicroom_button_list_code.append(f"{self.indent_str}xysize {musicroom_button_list.size}") + # 固定可拖拽 + musicroom_button_list_code.append(f"{self.indent_str}draggable True") + musicroom_button_list_code.append(f"{self.indent_str}mousewheel True") + # 鼠标放在viewport边缘自动滚动 + musicroom_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)") + musicroom_button_list_code.append(f"{self.indent_str}grid musicroom_view_column musicroom_view_row:") + self.indent_level_up() + musicroom_button_list_code.append(f"{self.indent_str}yspacing {musicroom_button_list.line_gap}") + musicroom_button_list_code.append(f"{self.indent_str}xspacing {musicroom_button_list.col_gap}") + if musicroom_button_list.layout == 'flow_vt' : + musicroom_button_list_code.append(f"{self.indent_str}transpose True") + # 若ui_helper.rpy不存在,则根据列表长度添加对应数量的默认按钮 + if not ui_helper_exists: + for i in range(len(musicroom_button_list.item_list)): + musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}()") + # 若ui_helper.rpy存在,则使用固定脚本定义的按钮 + else: + musicroom_button_list_code.append(f"{self.indent_str}for i in range(musicroom_button_num):") + self.indent_level_up() + musicroom_button_list_code.append(f"{self.indent_str}$ name, file = gallery_music_list[i]") + musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}(title=name, actions=SwitchMusicRoomPlay(file)))") + self.indent_level_down() + self.screen_ui_code.extend(musicroom_button_list_code) + self.indent_level_down(2) + # musicroom_button_list 之后的组件 + self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=musicroom_button_list_index+1)) + self.screen_ui_code.append("") + self.reset_indent_level() + + self.music_room_screen_code.extend(self.screen_definition_head) + self.music_room_screen_code.extend(self.screen_ui_code) + # 读取 templates/renpy_music_room_template.txt 模板内容,并替换 {music_room_screen_code} 为 self.music_room_screen_code + music_room_template_content = self.get_template_content('renpy_music_room_template.txt') + music_room_template_content = music_room_template_content.replace('{music_room_screen_code}', '\n'.join(self.music_room_screen_code)) + music_room_template_content = music_room_template_content.replace('{musicroom_button_list_len}', str(musicroom_button_list_len)) + music_room_template_content = music_room_template_content.replace('{musicroom_button_list_column}', str(musicroom_button_list_column)) + music_room_template_content = music_room_template_content.replace('{musicroom_button_list_row}', str(musicroom_button_list_row)) + self.music_room_screen_code = music_room_template_content.split('\n') + + # 将内容保存在 game/scripts/music_room_screen.rpy 文件中 + music_room_screen_file = os.path.join(self.game_dir, 'scripts', 'music_room_screen.rpy') + self.save_code_to_file(music_room_screen_file, self.music_room_screen_code) + print(f"Music room screen code has been saved to {music_room_screen_file}.") + return + + def generate_say_screen(self, component : FguiComponent): print("This is say screen.") self.screen_definition_head.clear() @@ -1707,7 +1839,7 @@ def generate_text_displayable_string(self, fgui_text): text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})" return text_displayable_string - def generate_image_displayable(self, fgui_image : FguiImage): + def generate_image_displayable(self, fgui_image : FguiImage) -> list: """ 生成图片组件。 前提为image对象的定义已经在generate_image_definitions中生成。 @@ -1738,7 +1870,7 @@ def generate_image_displayable(self, fgui_image : FguiImage): # Ren'Py中旋转轴心固定为图片中心(0.5,0.5)或与锚点一致,锚点可指定为任意值。 # 若要与FairyGUI资源保持一致,需设置offset。 # size可能为None,需要获取 - if not fgui_image.size: + if (not fgui_image.size) or (fgui_image.size == (0, 0)): size = self.fgui_assets.get_image_size_by_id(fgui_image.src) else: size = fgui_image.size @@ -2157,6 +2289,11 @@ def generate_list_displayable(self, fgui_list): def generate_renpy_code(self): """生成完整的Ren'Py代码""" + + + # 读取01_ui_helper.rpy文件 + self.gallery_data = self.get_gallery_data_from_ui_helper(os.path.join(self.game_dir, self.ui_helper_file_name)) + print(f"gallery_data: {self.gallery_data}") self.renpy_code = [] # 添加文件头注释 @@ -2235,6 +2372,67 @@ def get_template_content(self, filename : str, path : str = None) -> str: content = file.read() return content + def get_gallery_data_from_ui_helper(self, file_path: str) -> dict: + """ + 从01_ui_helper.rpy文件中读取以"define"开头的列表 + """ + result = {} + + if not os.path.exists(file_path): + print(f"文件不存在: {file_path}") + return result + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 使用正则表达式匹配 define 语句 + # 匹配格式: define variable_name = [...] + # 需要处理多行列表,通过匹配括号来找到完整的列表 + pattern = r'define\s+(\w+)\s*=\s*(\[)' + + matches = list(re.finditer(pattern, content)) + + for match in matches: + var_name = match.group(1) + list_start_pos = match.end() - 1 # '[' 的位置 + + # 从 '[' 开始,找到匹配的 ']' + bracket_count = 0 + i = list_start_pos + list_end_pos = -1 + + while i < len(content): + if content[i] == '[': + bracket_count += 1 + elif content[i] == ']': + bracket_count -= 1 + if bracket_count == 0: + list_end_pos = i + break + i += 1 + + if list_end_pos == -1: + print(f"无法找到 {var_name} 列表的结束位置") + continue + + # 提取列表字符串 + list_str = content[list_start_pos:list_end_pos + 1] + + try: + # 使用ast.literal_eval安全地解析列表 + list_value = ast.literal_eval(list_str) + result[var_name] = list_value + print(f"成功读取 define {var_name}: {len(list_value)} 个元素") + except (ValueError, SyntaxError) as e: + print(f"解析 {var_name} 时出错: {e}") + continue + + except Exception as e: + print(f"读取文件时出错: {e}") + + return result + def cleanup(self): """ 清理转换器资源 @@ -2397,12 +2595,13 @@ def convert(argv): # self.renpy_code.extend(self.graph_definition_code) # self.renpy_code.extend(self.style_code) converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code) + converter.image_definition_code.extend(converter.graph_definition_code) converter.save_code_to_file(image_definition_output_file, converter.image_definition_code) converter.save_code_to_file(style_output_file, converter.style_code) converter.save_code_to_file(screen_output_file, converter.screen_code) # 部分预定义模板文件修改参数并保存 - font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy") + font_map_definition_file = os.path.join(scripts_dir, "01_font_map.rpy") converter.from_templates_to_renpy(font_map_definition_file) # 复制预定义cdd和cds文件 diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt index f19f179..8c120d8 100644 --- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt @@ -21,7 +21,6 @@ init python: g.image(e) g.unlock(e) g.transform(gallery_full_screen) - # gallery_button_num = len(gallery_image_list) gallery_button_num = {gallery_button_list_len} g.transition = None gallery_view_column = {gallery_button_list_column} diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt new file mode 100644 index 0000000..fe71bfe --- /dev/null +++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt @@ -0,0 +1,29 @@ +init python: + + fadeout_time = 1.0 + class SwitchMusicRoomPlay(Action): + def __init__(self, file, **kwargs): + global fadeout_time + super(Action, self).__init__(**kwargs) + self.file = file + self.selected = self.get_selected() + self.fadeout_time = fadeout_time + def __call__(self): + if self.selected: + renpy.music.stop("music", fadeout=self.fadeout_time) + self.selected = not self.selected + else: + renpy.music.play(self.file, fadeout=self.fadeout_time) + def get_selected(self): + return renpy.music.get_playing("music") == self.file + def periodic(self, st): + if self.selected != self.get_selected(): + self.selected = self.get_selected() + renpy.restart_interaction() + return .1 + + musicroom_button_num = {musicroom_button_list_len} + musicroom_view_column = {musicroom_button_list_column} + musicroom_view_row = {musicroom_button_list_row} + +{music_room_screen_code} \ No newline at end of file