Fix Render Config UI Prototype Layout

This commit is contained in:
kyulio committed 2026-10-05 16:21:26 +09:00
1 parent 477b46a8dc
commit b6f1c0e4d2
1 file changed
+138 -112
+138 -112
View File
@@ -86,56 +86,50 @@ internal static class RenderConfigWindow
var config = RenderManager.Instance.Configuration ??= new RenderConfig(); var config = RenderManager.Instance.Configuration ??= new RenderConfig();
EnsureDefaultConfig(config); EnsureDefaultConfig(config);
// 1. RenderFPS & VideoFPS // 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개)
var (row1Left, row1Right) = CreateTwoColumnRow(scrollContent); var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f);
_renderFpsInput = CreateInputField( _renderFpsInput = CreateInputField(
ctx, row1Left, ctx, CreateFieldSlot(topLeft),
LocalizationManager.Get("ui.render_config.render_fps"), LocalizationManager.Get("ui.render_config.render_fps"),
"60", config.RenderFPS.ToString("0.##"), "60", config.RenderFPS.ToString("0.##"),
"input_render_fps", "input_render_fps",
TMPro.TMP_InputField.CharacterValidation.Decimal, TMPro.TMP_InputField.CharacterValidation.Decimal,
val => { if (double.TryParse(val, out var d) && d > 0) config.RenderFPS = d; } val => { if (double.TryParse(val, out var d) && d > 0) config.RenderFPS = d; }
); );
_videoFpsInput = CreateInputField(
ctx, row1Right,
LocalizationManager.Get("ui.render_config.video_fps"),
"60", config.VideoFPS.ToString("0.##"),
"input_video_fps",
TMPro.TMP_InputField.CharacterValidation.Decimal,
val => { if (double.TryParse(val, out var d) && d > 0) config.VideoFPS = d; }
);
// 2. RenderWidth & RenderHeight
var (row2Left, row2Right) = CreateTwoColumnRow(scrollContent);
_renderWidthInput = CreateInputField( _renderWidthInput = CreateInputField(
ctx, row2Left, ctx, CreateFieldSlot(topLeft),
LocalizationManager.Get("ui.render_config.render_width"), LocalizationManager.Get("ui.render_config.render_width"),
"1920", config.RenderWidth.ToString(), "1920", config.RenderWidth.ToString(),
"input_render_width", "input_render_width",
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var i) && i > 0) config.RenderWidth = i; } val => { if (int.TryParse(val, out var i) && i > 0) config.RenderWidth = i; }
); );
_renderHeightInput = CreateInputField(
ctx, row2Right,
LocalizationManager.Get("ui.render_config.render_height"),
"1080", config.RenderHeight.ToString(),
"input_render_height",
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var i) && i > 0) config.RenderHeight = i; }
);
// 3. Delay After Level End & Show Judgement
var (row3Left, row3Right) = CreateTwoColumnRow(scrollContent);
_delayAfterEndInput = CreateInputField( _delayAfterEndInput = CreateInputField(
ctx, row3Left, ctx, CreateFieldSlot(topLeft),
LocalizationManager.Get("ui.render_config.delay_after_end"), LocalizationManager.Get("ui.render_config.delay_after_end"),
"1.0", config.DelayAfterEnd.ToString("0.##"), "1.0", config.DelayAfterEnd.ToString("0.##"),
"input_delay_after_end", "input_delay_after_end",
TMPro.TMP_InputField.CharacterValidation.Decimal, TMPro.TMP_InputField.CharacterValidation.Decimal,
val => { if (double.TryParse(val, out var d) && d >= 0) config.DelayAfterEnd = d; } val => { if (double.TryParse(val, out var d) && d >= 0) config.DelayAfterEnd = d; }
); );
_videoFpsInput = CreateInputField(
ctx, CreateFieldSlot(topRight),
LocalizationManager.Get("ui.render_config.video_fps"),
"60", config.VideoFPS.ToString("0.##"),
"input_video_fps",
TMPro.TMP_InputField.CharacterValidation.Decimal,
val => { if (double.TryParse(val, out var d) && d > 0) config.VideoFPS = d; }
);
_renderHeightInput = CreateInputField(
ctx, CreateFieldSlot(topRight),
LocalizationManager.Get("ui.render_config.render_height"),
"1080", config.RenderHeight.ToString(),
"input_render_height",
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var i) && i > 0) config.RenderHeight = i; }
);
_showJudgementToggle = CreateToggleField( _showJudgementToggle = CreateToggleField(
ctx, row3Right, ctx, CreateFieldSlot(topRight),
LocalizationManager.Get("ui.render_config.show_judgement"), LocalizationManager.Get("ui.render_config.show_judgement"),
false, config.ShowJudgementText, false, config.ShowJudgementText,
LocalizationManager.Get("ui.render_config.show_judgement"), LocalizationManager.Get("ui.render_config.show_judgement"),
@@ -154,56 +148,52 @@ internal static class RenderConfigWindow
showActiveToggle: false showActiveToggle: false
); );
var (vRow1Left, vRow1Right) = CreateTwoColumnRow(videoContent); var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent);
var codecs = (Codec[])Enum.GetValues(typeof(Codec)); var codecs = (Codec[])Enum.GetValues(typeof(Codec));
_codecDropdown = CreateDropdownField( _codecDropdown = CreateDropdownField(
ctx, vRow1Left, ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.codec"), LocalizationManager.Get("ui.render_config.codec"),
Codec.H264, config.VideoConfig.Codec, Codec.H264, config.VideoConfig.Codec,
codecs, c => c.ToString(), codecs, c => c.ToString(),
"dd_video_codec", "dd_video_codec",
c => config.VideoConfig.Codec = c c => config.VideoConfig.Codec = c
); );
var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode));
_rateControlDropdown = CreateDropdownField(
ctx, vRow1Right,
LocalizationManager.Get("ui.render_config.rate_control"),
RateControlMode.CBR, config.VideoConfig.RateControlMode,
rateModes, m => m.ToString(),
"dd_rate_control",
m => config.VideoConfig.RateControlMode = m
);
var (vRow2Left, vRow2Right) = CreateTwoColumnRow(videoContent);
_bitrateInput = CreateInputField( _bitrateInput = CreateInputField(
ctx, vRow2Left, ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.bitrate"), LocalizationManager.Get("ui.render_config.bitrate"),
"30000", config.VideoConfig.Bitrate.ToString(), "30000", config.VideoConfig.Bitrate.ToString(),
"input_bitrate", "input_bitrate",
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; } val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; }
); );
_maxBitrateInput = CreateInputField(
ctx, vRow2Right,
LocalizationManager.Get("ui.render_config.max_bitrate"),
"50000", config.VideoConfig.MaxBitrate.ToString(),
"input_max_bitrate",
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; }
);
var (vRow3Left, vRow3Right) = CreateTwoColumnRow(videoContent);
_qpInput = CreateInputField( _qpInput = CreateInputField(
ctx, vRow3Left, ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.qp"), LocalizationManager.Get("ui.render_config.qp"),
"18", config.VideoConfig.QP.ToString(), "18", config.VideoConfig.QP.ToString(),
"input_qp", "input_qp",
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; } val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; }
); );
var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode));
_rateControlDropdown = CreateDropdownField(
ctx, CreateFieldSlot(videoRight),
LocalizationManager.Get("ui.render_config.rate_control"),
RateControlMode.CBR, config.VideoConfig.RateControlMode,
rateModes, m => m.ToString(),
"dd_rate_control",
m => config.VideoConfig.RateControlMode = m
);
_maxBitrateInput = CreateInputField(
ctx, CreateFieldSlot(videoRight),
LocalizationManager.Get("ui.render_config.max_bitrate"),
"50000", config.VideoConfig.MaxBitrate.ToString(),
"input_max_bitrate",
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; }
);
_keyframeIntervalInput = CreateInputField( _keyframeIntervalInput = CreateInputField(
ctx, vRow3Right, ctx, CreateFieldSlot(videoRight),
LocalizationManager.Get("ui.render_config.keyframe_interval"), LocalizationManager.Get("ui.render_config.keyframe_interval"),
"2.0", config.VideoConfig.KeyframeInterval.ToString("0.##"), "2.0", config.VideoConfig.KeyframeInterval.ToString("0.##"),
"input_keyframe_interval", "input_keyframe_interval",
@@ -230,38 +220,36 @@ internal static class RenderConfigWindow
showActiveToggle: false showActiveToggle: false
); );
var (aRow1Left, aRow1Right) = CreateTwoColumnRow(audioContent); var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent);
_renderAudioToggle = CreateToggleField( _renderAudioToggle = CreateToggleField(
ctx, aRow1Left, ctx, CreateFieldSlot(audioLeft),
LocalizationManager.Get("ui.render_config.render_audio"), LocalizationManager.Get("ui.render_config.render_audio"),
true, config.AudioConfig.RenderAudio, true, config.AudioConfig.RenderAudio,
LocalizationManager.Get("ui.render_config.render_audio"), LocalizationManager.Get("ui.render_config.render_audio"),
"toggle_render_audio", "toggle_render_audio",
val => config.AudioConfig.RenderAudio = val val => config.AudioConfig.RenderAudio = val
); );
var sampleRates = new[] { 44100, 48000 };
_sampleRateDropdown = CreateDropdownField(
ctx, aRow1Right,
LocalizationManager.Get("ui.render_config.sample_rate"),
44100, config.AudioConfig.AudioSampleRate,
sampleRates, r => $"{r} Hz",
"dd_sample_rate",
r => config.AudioConfig.AudioSampleRate = r
);
var (aRow2Left, aRow2Right) = CreateTwoColumnRow(audioContent);
var channels = new[] { 1, 2 }; var channels = new[] { 1, 2 };
_channelsDropdown = CreateDropdownField( _channelsDropdown = CreateDropdownField(
ctx, aRow2Left, ctx, CreateFieldSlot(audioLeft),
LocalizationManager.Get("ui.render_config.channels"), LocalizationManager.Get("ui.render_config.channels"),
2, config.AudioConfig.AudioChannels, 2, config.AudioConfig.AudioChannels,
channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)", channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)",
"dd_channels", "dd_channels",
c => config.AudioConfig.AudioChannels = c c => config.AudioConfig.AudioChannels = c
); );
var sampleRates = new[] { 44100, 48000 };
_sampleRateDropdown = CreateDropdownField(
ctx, CreateFieldSlot(audioRight),
LocalizationManager.Get("ui.render_config.sample_rate"),
44100, config.AudioConfig.AudioSampleRate,
sampleRates, r => $"{r} Hz",
"dd_sample_rate",
r => config.AudioConfig.AudioSampleRate = r
);
_audioBitrateInput = CreateInputField( _audioBitrateInput = CreateInputField(
ctx, aRow2Right, ctx, CreateFieldSlot(audioRight),
LocalizationManager.Get("ui.render_config.audio_bitrate"), LocalizationManager.Get("ui.render_config.audio_bitrate"),
"256", (config.AudioConfig.AudioBitrate / 1000).ToString(), "256", (config.AudioConfig.AudioBitrate / 1000).ToString(),
"input_audio_bitrate", "input_audio_bitrate",
@@ -301,9 +289,9 @@ internal static class RenderConfigWindow
} }
); );
var (gBtnLeft, gBtnRight) = CreateTwoColumnRow(globalContent); var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent);
O5Factory.Button( O5Factory.Button(
ctx, gBtnLeft, ctx, globalLeft,
() => () =>
{ {
var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value) var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value)
@@ -324,7 +312,7 @@ internal static class RenderConfigWindow
); );
O5Factory.Button( O5Factory.Button(
ctx, gBtnRight, ctx, globalRight,
() => () =>
{ {
_outputDirInput.Set(GlobalSettings.DefaultOutputDirectory); _outputDirInput.Set(GlobalSettings.DefaultOutputDirectory);
@@ -346,19 +334,27 @@ internal static class RenderConfigWindow
showActiveToggle: false showActiveToggle: false
); );
var (adRow1Left, adRow1Right) = CreateTwoColumnRow(advContent); var (advLeft, advRight) = CreateTwoColumnPanel(advContent);
var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" };
_fileExtensionDropdown = CreateDropdownField( _fileExtensionDropdown = CreateDropdownField(
ctx, adRow1Left, ctx, CreateFieldSlot(advLeft),
LocalizationManager.Get("ui.render_config.file_extension"), LocalizationManager.Get("ui.render_config.file_extension"),
"mkv", config.AdvancedConfig.FileExtension, "mkv", config.AdvancedConfig.FileExtension,
extensions, e => e, extensions, e => e,
"dd_adv_file_extension", "dd_adv_file_extension",
e => { if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e; } e => { if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e; }
); );
_processAudioToggle = CreateToggleField(
ctx, CreateFieldSlot(advLeft),
LocalizationManager.Get("ui.render_config.process_audio"),
true, config.AdvancedConfig.ProcessAudio,
LocalizationManager.Get("ui.render_config.process_audio"),
"toggle_process_audio",
val => config.AdvancedConfig.ProcessAudio = val
);
_randomSeedInput = CreateInputField( _randomSeedInput = CreateInputField(
ctx, adRow1Right, ctx, CreateFieldSlot(advRight),
LocalizationManager.Get("ui.render_config.random_seed"), LocalizationManager.Get("ui.render_config.random_seed"),
"", config.AdvancedConfig.RandomSeed?.ToString() ?? "", "", config.AdvancedConfig.RandomSeed?.ToString() ?? "",
"input_random_seed", "input_random_seed",
@@ -369,18 +365,8 @@ internal static class RenderConfigWindow
else if (string.IsNullOrWhiteSpace(val)) config.AdvancedConfig.RandomSeed = null; else if (string.IsNullOrWhiteSpace(val)) config.AdvancedConfig.RandomSeed = null;
} }
); );
var (adRow2Left, adRow2Right) = CreateTwoColumnRow(advContent);
_processAudioToggle = CreateToggleField(
ctx, adRow2Left,
LocalizationManager.Get("ui.render_config.process_audio"),
true, config.AdvancedConfig.ProcessAudio,
LocalizationManager.Get("ui.render_config.process_audio"),
"toggle_process_audio",
val => config.AdvancedConfig.ProcessAudio = val
);
_nonTileToggle = CreateToggleField( _nonTileToggle = CreateToggleField(
ctx, adRow2Right, ctx, CreateFieldSlot(advRight),
LocalizationManager.Get("ui.render_config.non_tile"), LocalizationManager.Get("ui.render_config.non_tile"),
false, config.AdvancedConfig.NonTile, false, config.AdvancedConfig.NonTile,
LocalizationManager.Get("ui.render_config.non_tile"), LocalizationManager.Get("ui.render_config.non_tile"),
@@ -564,41 +550,81 @@ internal static class RenderConfigWindow
); );
} }
private static (RectTransform left, RectTransform right) CreateTwoColumnRow(Transform parent, float spacing = 8f) // 카드(또는 상단 섹션)당 하나의 좌/우 컨테이너를 만들어
// 가로 한 줄 두 필드 사이 중앙이 행마다 어긋나지 않도록 한다.
// - 패널 1개 = 중앙선 1개 (행마다 HLG를 만들던 기존 방식의 P1-P2 어긋남 방지)
// - 좌/우 컬럼은 preferredWidth=0 + flexibleWidth=1 로 항상 50:50
// - 각 필드는 Field 슬롯(label 18 + 간격 3 + 컨트롤 50)으로 높이를 고정해
// 좌/우 스택을 써도 행 단위 Y 정렬이 유지되도록 한다.
private static (RectTransform left, RectTransform right) CreateTwoColumnPanel(
Transform parent, float spacing = 8f, float columnSpacing = 8f)
{ {
var rowObj = new GameObject("TwoColRow"); var panelObj = new GameObject("TwoColPanel");
rowObj.transform.SetParent(parent, false); panelObj.transform.SetParent(parent, false);
var rowRect = rowObj.AddComponent<RectTransform>(); panelObj.AddComponent<RectTransform>();
var hLayout = rowObj.AddComponent<HorizontalLayoutGroup>(); var hLayout = panelObj.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = spacing; hLayout.spacing = spacing;
hLayout.padding = new RectOffset(0, 0, 0, 0);
hLayout.childAlignment = TextAnchor.UpperCenter;
hLayout.childControlWidth = true; hLayout.childControlWidth = true;
hLayout.childControlHeight = true; hLayout.childControlHeight = true;
hLayout.childForceExpandWidth = true; hLayout.childForceExpandWidth = true;
hLayout.childForceExpandHeight = false; hLayout.childForceExpandHeight = false;
var col1Obj = new GameObject("Col1"); var panelLE = panelObj.AddComponent<LayoutElement>();
col1Obj.transform.SetParent(rowObj.transform, false); panelLE.flexibleWidth = 1f;
var col1Rect = col1Obj.AddComponent<RectTransform>();
var col1Layout = col1Obj.AddComponent<VerticalLayoutGroup>();
col1Layout.spacing = 3f;
col1Layout.childControlWidth = true;
col1Layout.childControlHeight = true;
col1Layout.childForceExpandWidth = true;
col1Layout.childForceExpandHeight = false;
var col2Obj = new GameObject("Col2"); var left = CreateColumn(panelObj.transform, "LeftCol", columnSpacing);
col2Obj.transform.SetParent(rowObj.transform, false); var right = CreateColumn(panelObj.transform, "RightCol", columnSpacing);
var col2Rect = col2Obj.AddComponent<RectTransform>();
var col2Layout = col2Obj.AddComponent<VerticalLayoutGroup>();
col2Layout.spacing = 3f;
col2Layout.childControlWidth = true;
col2Layout.childControlHeight = true;
col2Layout.childForceExpandWidth = true;
col2Layout.childForceExpandHeight = false;
return (col1Rect, col2Rect); return (left, right);
}
private static RectTransform CreateColumn(Transform parent, string name, float spacing)
{
var colObj = new GameObject(name);
colObj.transform.SetParent(parent, false);
var colRect = colObj.AddComponent<RectTransform>();
var colLayout = colObj.AddComponent<VerticalLayoutGroup>();
colLayout.spacing = spacing;
colLayout.padding = new RectOffset(0, 0, 0, 0);
colLayout.childControlWidth = true;
colLayout.childControlHeight = true;
colLayout.childForceExpandWidth = true;
colLayout.childForceExpandHeight = false;
// 핵심: 좌/우 선호 너비를 0으로 강제해 항상 정확히 절반씩 나누도록 한다.
// (기존 행마다 HLG 방식은 라벨/값 텍스트 길이에 따라 preferred가 달라져
// 중앙이 (W + P1 - P2) / 2 로 행마다 틀어졌음)
var colLE = colObj.AddComponent<LayoutElement>();
colLE.minWidth = 0f;
colLE.preferredWidth = 0f;
colLE.flexibleWidth = 1f;
return colRect;
}
private static RectTransform CreateFieldSlot(Transform column)
{
var slotObj = new GameObject("Field");
slotObj.transform.SetParent(column, false);
var slotRect = slotObj.AddComponent<RectTransform>();
var slotLayout = slotObj.AddComponent<VerticalLayoutGroup>();
slotLayout.spacing = 3f;
slotLayout.padding = new RectOffset(0, 0, 0, 0);
slotLayout.childControlWidth = true;
slotLayout.childControlHeight = true;
slotLayout.childForceExpandWidth = true;
slotLayout.childForceExpandHeight = false;
var slotLE = slotObj.AddComponent<LayoutElement>();
slotLE.flexibleWidth = 1f;
return slotRect;
} }
private static O5InputField CreateInputField( private static O5InputField CreateInputField(