Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
48c1d40
Add Fgui-Converter Entry
CursedOctopus Sep 22, 2025
29966ac
Add fgui_converter modul.
CursedOctopus Sep 29, 2025
f3b7a1c
Add independency in setup.cfg
CursedOctopus Oct 17, 2025
23055c0
Modify README.md, add `-o` option to `pyside6-uic` command.
CursedOctopus Oct 17, 2025
202c17e
Add null "input/output" popup and fix some Converter bug
CursedOctopus Oct 18, 2025
1d39189
Restructured button converter method
CursedOctopus Nov 4, 2025
7ddf56e
Add text style within Button
CursedOctopus Nov 4, 2025
9ae8262
Update Text drop-shadow outline method
CursedOctopus Nov 5, 2025
e5cfdb3
Support "Slider" component
CursedOctopus Nov 7, 2025
045a305
Support sliders in screen
CursedOctopus Nov 9, 2025
f46900f
Add special screens (choice, say) converting method.
CursedOctopus Nov 11, 2025
2fbde7a
Some component, as button and slider, could get custom data from sour…
CursedOctopus Nov 11, 2025
a215d68
Add special screens (say, save, load) converting method.
CursedOctopus Nov 12, 2025
b8082cf
Add special screens (history, gallery) converting method.
CursedOctopus Nov 13, 2025
c642ed8
Children of screen could be shown or hide by Fgui controllers.
CursedOctopus Nov 13, 2025
93e0f97
Fix issue of save/load screen lack of tag menu.
CursedOctopus Nov 14, 2025
31355b8
Implementation hidden(part display) and scrollable with screen level.
CursedOctopus Dec 8, 2025
926b585
Implementation of screen "gallery".
CursedOctopus Jan 6, 2026
dc4f1f2
Add music_room screen implementation
CursedOctopus Jan 10, 2026
f2214a5
Implementation converting of ScrollBar
CursedOctopus Jan 24, 2026
60929e3
Fix issue of bounding-box size with component.
CursedOctopus Feb 2, 2026
879b365
Fgui assets filter initial value fixed to (0,0,0,0) (changes made by …
xushengj Feb 15, 2026
66dc3b0
Add WIP warning for FGUI converter tool
xushengj Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,3 @@ thumbnails

# version files for pyinstaller build
versionfile_*.txt

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,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`。其他环境下可使用同目录下的 `makeall.py`。该操作需要在任意 .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启动

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ install_requires =
xlsxwriter
psd-tools <= 1.10.9
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
Expand Down
1,158 changes: 1,158 additions & 0 deletions src/fgui_converter/FguiAssetsParseLib.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/fgui_converter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-

# 将该目录视为可导入的 Python 包。

2,716 changes: 2,716 additions & 0 deletions src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy
Original file line number Diff line number Diff line change
@@ -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]
99 changes: 99 additions & 0 deletions src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
Original file line number Diff line number Diff line change
@@ -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));
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));
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));
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));
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);
""")
Original file line number Diff line number Diff line change
@@ -0,0 +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)
xysize {xysize}
pos {pos}
anchor {anchor}
xoffset {xoffset}
yoffset {yoffset}
transform_anchor {transform_anchor}
rotate {rotate}
alpha {alpha}
xzoom {xzoom}
yzoom {yzoom}
Original file line number Diff line number Diff line change
@@ -0,0 +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})
xysize {xysize}
pos {pos}
anchor {anchor}
xoffset {xoffset}
yoffset {yoffset}
transform_anchor {transform_anchor}
rotate {rotate}
alpha {alpha}
xzoom {xzoom}
yzoom {yzoom}
Original file line number Diff line number Diff line change
@@ -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}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 = {gallery_button_list_len}
g.transition = None
gallery_view_column = {gallery_button_list_column}
gallery_view_row = {gallery_button_list_row}

{gallery_screen_code}
Loading
Loading