Files
ai-images/Source/AIImages/Window_AIImage.cs

924 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Linq;
using System.Threading;
using AIImages.Helpers;
using AIImages.Models;
using AIImages.Services;
using RimWorld;
using UnityEngine;
using Verse;
#pragma warning disable IDE1006 // Naming Styles
namespace AIImages
{
/// <summary>
/// Окно для просмотра персонажа и генерации AI изображений
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Style",
"IDE1006:Naming Styles",
Justification = "RimWorld Window naming convention"
)]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Minor Code Smell",
"S101:Types should be named in PascalCase",
Justification = "RimWorld Window naming convention"
)]
public class Window_AIImage : Window
{
private Pawn pawn;
private PawnAppearanceData appearanceData;
private StableDiffusionSettings generationSettings;
private Texture2D generatedImage;
private bool isGenerating = false;
private string generationStatus = "";
private CancellationTokenSource cancellationTokenSource;
// Прогресс генерации
private double generationProgress = 0.0;
private int currentStep = 0;
private int totalSteps = 0;
private double etaSeconds = 0.0;
// Сервисы (получаем через DI)
private readonly IPawnDescriptionService pawnDescriptionService;
private readonly IPromptGeneratorService promptGeneratorService;
private readonly IStableDiffusionApiService apiService;
public Window_AIImage(Pawn pawn)
{
this.pawn = pawn;
this.doCloseX = true;
this.doCloseButton = false; // Убираем дублирующую кнопку "Закрыть"
this.forcePause = false;
this.absorbInputAroundWindow = false;
this.draggable = true;
this.preventCameraMotion = false;
// Получаем сервисы через DI контейнер
var services = AIImagesMod.Services;
pawnDescriptionService = services.PawnDescriptionService;
promptGeneratorService = services.PromptGeneratorService;
apiService = services.ApiService;
// Извлекаем данные персонажа
RefreshPawnData();
}
public override Vector2 InitialSize => new Vector2(900f, 800f);
private Vector2 mainScrollPosition = Vector2.zero;
private Vector2 promptScrollPosition = Vector2.zero;
private Vector2 negativePromptScrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
// Состояние сворачиваемых секций промптов
private bool showPositivePrompt = false;
private bool showNegativePrompt = false;
/// <summary>
/// Обновляет данные персонажа
/// </summary>
private void RefreshPawnData()
{
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
// Загружаем сохраненный портрет, если есть
LoadSavedPortrait();
}
/// <summary>
/// Загружает сохраненный портрет персонажа
/// </summary>
private void LoadSavedPortrait()
{
if (PawnPortraitHelper.HasPortrait(pawn))
{
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
if (generatedImage != null)
{
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
}
}
}
/// <summary>
/// Освобождает ресурсы при закрытии окна
/// </summary>
public override void PreClose()
{
base.PreClose();
// Отменяем генерацию, если она идет
if (isGenerating && cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
}
// Освобождаем CancellationTokenSource
cancellationTokenSource?.Dispose();
}
/// <summary>
/// Обновляет текущую пешку в окне
/// </summary>
public void UpdatePawn(Pawn newPawn)
{
this.pawn = newPawn;
RefreshPawnData();
// Очищаем старое изображение при смене персонажа
generatedImage = null;
generationStatus = "";
generationProgress = 0.0;
}
/// <summary>
/// Получить текущую пешку
/// </summary>
public Pawn CurrentPawn => pawn;
/// <summary>
/// Вызывается каждый кадр для обновления окна
/// </summary>
public override void WindowUpdate()
{
base.WindowUpdate();
// Проверяем, изменилась ли выбранная пешка
Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p =>
p.IsColonist && p.Spawned && p.Faction == Faction.OfPlayer
);
// Если выбрана новая колонистская пешка, обновляем окно
if (selectedPawn != null && selectedPawn != pawn)
{
UpdatePawn(selectedPawn);
}
// Уменьшаем таймер сообщения о копировании
if (copiedMessageTime > 0f)
{
copiedMessageTime -= Time.deltaTime;
}
}
/// <summary>
/// Асинхронная генерация изображения с поддержкой отмены
/// </summary>
private async System.Threading.Tasks.Task GenerateImageAsync()
{
if (isGenerating)
return;
// Создаем новый CancellationTokenSource
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
generationProgress = 0.0;
currentStep = 0;
totalSteps = generationSettings.Steps;
try
{
// Генерируем промпты
string positivePrompt = promptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
string negativePrompt = promptGeneratorService.GenerateNegativePrompt(
generationSettings
);
// Создаем запрос
var request = new GenerationRequest
{
Prompt = positivePrompt,
NegativePrompt = negativePrompt,
Steps = generationSettings.Steps,
CfgScale = generationSettings.CfgScale,
Width = generationSettings.Width,
Height = generationSettings.Height,
Sampler = generationSettings.Sampler,
Scheduler = generationSettings.Scheduler,
Seed = generationSettings.Seed,
Model = AIImagesMod.Settings.apiEndpoint,
SaveImagesToServer = AIImagesMod.Settings.saveImagesToServer,
};
// Создаем отдельный CancellationTokenSource для мониторинга прогресса
var progressCts = new CancellationTokenSource();
var progressTask = MonitorProgressAsync(progressCts.Token);
// Генерируем изображение с поддержкой отмены
var result = await apiService.GenerateImageAsync(
request,
cancellationTokenSource.Token
);
// Останавливаем мониторинг прогресса
progressCts.Cancel();
try
{
await progressTask;
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке мониторинга
}
finally
{
progressCts.Dispose();
}
if (result.Success)
{
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
generationProgress = 1.0;
// Сохраняем путь к портрету на персонаже
PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
Messages.Message(
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
MessageTypeDefOf.PositiveEvent
);
}
else
{
generationStatus =
$"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
}
}
catch (OperationCanceledException)
{
generationStatus = "AIImages.Generation.Cancelled".Translate();
Log.Message("[AI Images] Generation cancelled by user");
}
catch (Exception ex)
{
generationStatus = $"Error: {ex.Message}";
Log.Error($"[AI Images] Generation error: {ex}");
Messages.Message(
$"AIImages.Generation.Error".Translate() + ": {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
finally
{
isGenerating = false;
}
}
/// <summary>
/// Мониторит прогресс генерации и обновляет UI
/// </summary>
private async System.Threading.Tasks.Task MonitorProgressAsync(
CancellationToken cancellationToken
)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var progress = await apiService.GetProgressAsync(cancellationToken);
if (progress != null && progress.IsActive)
{
generationProgress = progress.Progress;
currentStep = progress.CurrentStep;
totalSteps = progress.TotalSteps;
etaSeconds = progress.EtaRelative;
Log.Message(
$"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s"
);
}
// Обновляем каждые 500ms
await System.Threading.Tasks.Task.Delay(500, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
}
}
/// <summary>
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
/// </summary>
private void StartGeneration()
{
AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation");
}
/// <summary>
/// Отменяет генерацию изображения
/// </summary>
private void CancelGeneration()
{
if (isGenerating && cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
generationStatus = "AIImages.Generation.Cancelling".Translate();
}
}
public override void DoWindowContents(Rect inRect)
{
// Рассчитываем общую высоту контента
float totalContentHeight = CalculateTotalContentHeight();
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, totalContentHeight);
Widgets.BeginScrollView(inRect, ref mainScrollPosition, viewRect);
float curY = 0f;
// Заголовок
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(0f, curY, viewRect.width, 40f),
"AIImages.Window.Title".Translate()
);
curY += 45f;
// Имя пешки
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(0f, curY, viewRect.width, 30f),
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
);
curY += 40f;
// Разделитель
Widgets.DrawLineHorizontal(0f, curY, viewRect.width);
curY += 10f;
// Разделяем на две колонки: левая - информация, правая - изображение
float leftColumnWidth = viewRect.width * 0.35f;
float rightColumnWidth = viewRect.width * 0.62f;
float columnGap = viewRect.width * 0.03f;
// Определяем высоту колонок (берем большую из двух)
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
// Левая колонка - информация
Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, columnHeight);
DrawLeftColumn(leftColumnRect);
// Правая колонка - превью и управление
Rect rightColumnRect = new Rect(
leftColumnWidth + columnGap,
curY,
rightColumnWidth,
columnHeight
);
DrawRightColumn(rightColumnRect);
Widgets.EndScrollView();
}
private void DrawLeftColumn(Rect rect)
{
float contentY = rect.y;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = Mathf.Min(rect.width, 250f);
Rect portraitRect = new Rect(
rect.x + (rect.width - portraitSize) / 2f,
contentY,
portraitSize,
portraitSize
);
// Рамка вокруг портрета
Widgets.DrawBox(portraitRect);
GUI.DrawTexture(portraitRect.ContractedBy(2f), generatedImage);
contentY += portraitSize + 15f;
}
// Заголовок секции
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(rect.x, contentY, rect.width, 30f),
"AIImages.CharacterInfo.Title".Translate()
);
contentY += 35f;
// Разделитель
Widgets.DrawLineHorizontal(rect.x, contentY, rect.width);
contentY += 10f;
// Информация о персонаже
DrawCharacterInfoContent(rect, contentY);
}
/// <summary>
/// Отрисовывает информацию о персонаже в компактном виде
/// </summary>
private void DrawCharacterInfoContent(Rect parentRect, float startY)
{
float contentY = startY;
float lineHeight = 22f;
float labelWidth = parentRect.width * 0.45f;
float valueWidth = parentRect.width * 0.50f;
Text.Font = GameFont.Tiny;
// Базовая информация
var info = new[]
{
("AIImages.Info.Gender".Translate(), appearanceData.Gender.ToString()),
("AIImages.Info.Age".Translate(), appearanceData.Age.ToString()),
("AIImages.Info.BodyType".Translate(), appearanceData.BodyType),
(
"AIImages.Info.SkinTone".Translate(),
ColorDescriptionService.GetSkinToneDescription(appearanceData.SkinColor)
),
("AIImages.Info.Hair".Translate(), appearanceData.HairStyle),
(
"AIImages.Info.HairColor".Translate(),
ColorDescriptionService.GetHairColorDescription(appearanceData.HairColor)
),
};
foreach (var (label, value) in info)
{
// Подсветка строк через одну
var infoArray = info.ToArray();
int index = Array.IndexOf(infoArray, (label, value));
if ((index % 2) == 0)
{
Widgets.DrawBoxSolid(
new Rect(parentRect.x, contentY, parentRect.width, lineHeight),
new Color(0.15f, 0.15f, 0.15f, 0.3f)
);
}
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, labelWidth, lineHeight),
label + ":"
);
Widgets.Label(
new Rect(parentRect.x + labelWidth + 10f, contentY, valueWidth, lineHeight),
value
);
contentY += lineHeight;
}
// Черты характера
if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
contentY += 15f;
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
"AIImages.Info.Traits".Translate() + ":"
);
contentY += lineHeight + 2f;
Text.Font = GameFont.Tiny;
var traitLabels = pawn.story.traits.allTraits.Select(trait => trait.LabelCap);
foreach (var traitLabel in traitLabels)
{
Widgets.Label(
new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight),
"• " + traitLabel
);
contentY += lineHeight;
}
}
// Одежда
var apparel = pawn.apparel?.WornApparel;
if (apparel != null && apparel.Any())
{
contentY += 15f;
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
"AIImages.Info.Apparel".Translate() + ":"
);
contentY += lineHeight + 2f;
Text.Font = GameFont.Tiny;
foreach (var item in apparel)
{
var colorDesc = ColorDescriptionService.GetApparelColorDescription(
item.DrawColor
);
string apparelLabel = $"• {colorDesc} {item.def.label}";
float apparelHeight = Text.CalcHeight(apparelLabel, parentRect.width - 25f);
Widgets.Label(
new Rect(
parentRect.x + 15f,
contentY,
parentRect.width - 25f,
apparelHeight
),
apparelLabel
);
contentY += apparelHeight;
}
}
}
private void DrawRightColumn(Rect rect)
{
float curY = rect.y;
curY = DrawImagePreview(rect, curY);
curY = DrawGenerationStatus(rect, curY);
curY = DrawProgressBar(rect, curY);
curY = DrawGenerationButton(rect, curY);
// Сворачиваемая секция с позитивным промптом
curY = DrawPromptSection(
rect,
curY,
"AIImages.Prompt.PositiveTitle".Translate(),
ref showPositivePrompt,
() =>
promptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
),
new Color(0.1f, 0.3f, 0.1f, 0.5f),
ref promptScrollPosition
);
curY += 5f;
// Сворачиваемая секция с негативным промптом
curY = DrawPromptSection(
rect,
curY,
"AIImages.Prompt.NegativeTitle".Translate(),
ref showNegativePrompt,
() => promptGeneratorService.GenerateNegativePrompt(generationSettings),
new Color(0.3f, 0.1f, 0.1f, 0.5f),
ref negativePromptScrollPosition
);
curY += 10f;
// Кнопка обновления данных
if (
Widgets.ButtonText(
new Rect(rect.x, curY, rect.width, 30f),
"AIImages.Window.Refresh".Translate()
)
)
{
RefreshPawnData();
}
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
curY += 35f;
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
Widgets.Label(
new Rect(rect.x, curY, rect.width, 25f),
"AIImages.Prompt.Copied".Translate()
);
GUI.color = Color.white;
}
}
private float CalculateContentHeight()
{
float height = 0f;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = 250f; // Максимальный размер портрета
height += portraitSize + 15f;
}
// Заголовок "Информация о персонаже"
height += 35f;
// Разделитель
height += 10f;
// Базовая информация (6 строк по 22px)
height += 6 * 22f;
// Черты характера (если есть)
if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
height += 15f; // Отступ
height += 22f; // Заголовок "Черты характера"
height += 2f; // Отступ
height += pawn.story.traits.allTraits.Count * 22f; // Каждая черта
}
// Одежда (если есть)
var apparel = pawn.apparel?.WornApparel;
if (apparel != null && apparel.Any())
{
height += 15f; // Отступ
height += 22f; // Заголовок "Одежда"
height += 2f; // Отступ
// Примерно по 22-30px на предмет одежды
height += apparel.Count * 26f;
}
// Дополнительный отступ
height += 50f;
return height;
}
private float CalculateRightColumnHeight()
{
float height = 0f;
// Превью изображения
if (generatedImage != null)
{
height += 410f; // 400f изображение + 10f отступ
}
else if (!isGenerating)
{
height += 310f; // 300f placeholder + 10f отступ
}
// Статус генерации
if (!string.IsNullOrEmpty(generationStatus))
{
height += 30f;
}
// Прогресс бар
if (isGenerating && generationProgress > 0.0)
{
height += 30f;
}
// Кнопка генерации
height += 40f;
// Позитивный промпт
height += 36f; // Заголовок (32f + 4f отступ)
if (showPositivePrompt)
{
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
}
height += 5f; // Отступ между промптами
// Негативный промпт
height += 36f; // Заголовок (32f + 4f отступ)
if (showNegativePrompt)
{
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
}
height += 10f; // Отступ после промптов
// Кнопка обновления
height += 40f; // Увеличено с 35f до 40f
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
height += 40f;
}
return height + 100f; // Увеличен дополнительный отступ снизу
}
private float CalculateTotalContentHeight()
{
float height = 0f;
// Заголовок и имя пешки
height += 45f + 40f;
// Разделитель
height += 10f;
// Высота колонок (берем большую)
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
height += columnHeight;
// Дополнительный отступ снизу
height += 30f;
return height;
}
/// <summary>
/// /// Отрисовывает секцию с промптом
/// </summary>
private float DrawPromptSection(
Rect parentRect,
float startY,
string title,
ref bool expanded,
System.Func<string> getPromptFunc,
Color backgroundColor,
ref Vector2 scrollPosition
)
{
float curY = startY;
float headerHeight = 32f;
// Получаем промпт
string prompt = getPromptFunc();
// Рисуем заголовок с фоном
Rect headerRect = new Rect(parentRect.x, curY, parentRect.width, headerHeight);
Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
Widgets.DrawBox(headerRect);
// Иконка раскрытия
string icon = expanded ? "▼" : "►";
Text.Font = GameFont.Small;
Widgets.Label(new Rect(parentRect.x + 8f, curY + 6f, 20f, headerHeight), icon);
// Заголовок
Widgets.Label(
new Rect(parentRect.x + 30f, curY + 6f, parentRect.width - 110f, headerHeight),
title
);
// Кнопка копирования в заголовке (увеличена ширина)
Rect copyButtonRect = new Rect(
parentRect.x + parentRect.width - 100f,
curY + 4f,
95f,
24f
);
if (Widgets.ButtonText(copyButtonRect, "📋 " + "AIImages.Copy".Translate()))
{
GUIUtility.systemCopyBuffer = prompt;
copiedMessageTime = 2f;
}
// Клик на остальной области для раскрытия/сворачивания
Rect clickableHeaderRect = new Rect(
parentRect.x,
curY,
parentRect.width - 105f,
headerHeight
);
if (Widgets.ButtonInvisible(clickableHeaderRect))
{
expanded = !expanded;
}
curY += headerHeight + 4f;
// Рисуем содержимое если развернуто
if (expanded)
{
float promptBoxHeight = 120f;
Text.Font = GameFont.Tiny;
float actualPromptHeight = Text.CalcHeight(prompt, parentRect.width - 20f);
Rect promptOuterRect = new Rect(
parentRect.x,
curY,
parentRect.width,
promptBoxHeight
);
Rect promptViewRect = new Rect(0f, 0f, parentRect.width - 20f, actualPromptHeight);
// Рисуем фон
Widgets.DrawBoxSolid(promptOuterRect, backgroundColor);
Widgets.DrawBox(promptOuterRect);
// Рисуем промпт с прокруткой
Widgets.BeginScrollView(
promptOuterRect.ContractedBy(5f),
ref scrollPosition,
promptViewRect
);
var prevAnchor = Text.Anchor;
Text.Anchor = TextAnchor.UpperLeft;
Text.WordWrap = true;
Widgets.Label(
new Rect(0f, 0f, promptViewRect.width, promptViewRect.height),
prompt
);
Text.Anchor = prevAnchor;
Text.WordWrap = true;
Widgets.EndScrollView();
curY += promptBoxHeight + 10f;
}
return curY;
}
private float DrawImagePreview(Rect rect, float curY)
{
if (generatedImage != null)
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
rect.x + (rect.width - imageSize) / 2f,
curY,
imageSize,
imageSize
);
GUI.DrawTexture(imageRect, generatedImage);
return curY + imageSize + 10f;
}
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
rect.x + (rect.width - placeholderSize) / 2f,
curY,
placeholderSize,
placeholderSize
);
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
Text.Anchor = TextAnchor.UpperLeft;
return curY + placeholderSize + 10f;
}
private float DrawGenerationStatus(Rect rect, float curY)
{
if (string.IsNullOrEmpty(generationStatus))
return curY;
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(new Rect(rect.x, curY, rect.width, statusHeight), generationStatus);
return curY + statusHeight + 10f;
}
private float DrawProgressBar(Rect rect, float curY)
{
if (!isGenerating || generationProgress <= 0.0)
return curY;
Rect progressBarRect = new Rect(rect.x, curY, rect.width, 24f);
string progressText;
if (totalSteps > 0)
{
progressText =
$"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}";
if (etaSeconds > 0)
{
progressText += $" - ETA: {etaSeconds:F0}s";
}
}
else
{
progressText = $"{(generationProgress * 100):F1}%";
}
Widgets.FillableBar(progressBarRect, (float)generationProgress);
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(progressBarRect, progressText);
Text.Anchor = TextAnchor.UpperLeft;
return curY + 30f;
}
private float DrawGenerationButton(Rect rect, float curY)
{
Text.Font = GameFont.Small;
string buttonLabel = isGenerating
? "AIImages.Generation.Cancel".Translate()
: "AIImages.Generation.Generate".Translate();
if (Widgets.ButtonText(new Rect(rect.x, curY, rect.width, 35f), buttonLabel))
{
if (isGenerating)
CancelGeneration();
else
StartGeneration();
}
return curY + 40f;
}
}
}