using System;
using System.Collections.Generic;
using System.Text.Json;
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.UI.Lights;
using Cthangover.Core.UI.Theming;
using Cthangover.Core.Utils;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.LightEditor
{
///
/// Editor window for placing and configuring static lights on a background.
/// Provides a sidebar with background selection, a light list with add/delete/select,
/// per-light property editors (radius, influence, colour), draggable handles on a
/// shader-powered preview, and JSON export. Uses
/// for data management and for background textures.
///
public partial class LightEditorWindow : ToolWindow
{
private LightEditorController controller;
private ItemList backgroundList;
private VBoxContainer lightListPanel;
private Button addLightBtn;
private TextureRect preview;
private ShaderMaterial previewMaterial;
private LineEdit radiusEdit;
private LineEdit influenceEdit;
private ColorPickerButton colorPicker;
private TextEdit jsonEdit;
private readonly List handles = new();
private int selectedIndex = -1;
private readonly List bgIds = new();
/// Constructs the window, creates the controller, builds UI, and loads the background list.
public LightEditorWindow() : base("tools/light_editor/title")
{
controller = new LightEditorController();
CthangoverTheme.Apply(this);
BuildUI();
LoadBackgroundList();
}
protected override void Cleanup()
{
foreach (var h in handles)
h.QueueFree();
handles.Clear();
}
private Vector2 PreviewSize()
{
var sz = preview.Size;
if (sz.X <= 0 || sz.Y <= 0)
sz = new Vector2(1920, 1080);
return sz;
}
private void RefreshAll()
{
RepositionHandles();
controller.MarkDirty();
controller.UpdatePreview(previewMaterial, preview.GlobalPosition, PreviewSize());
UpdateLightList();
UpdateJsonEdit();
}
private void BuildUI()
{
var outerVBox = CreateFillContainer();
AddChild(outerVBox);
var mainHBox = new HBoxContainer();
mainHBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
mainHBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
outerVBox.AddChild(mainHBox);
var sidebarVBox = new VBoxContainer();
var sidebar = CreateSidebar(sidebarVBox);
mainHBox.AddChild(sidebar);
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/background") });
backgroundList = new ItemList();
backgroundList.CustomMinimumSize = new Vector2(0, 140);
backgroundList.ItemSelected += OnBackgroundSelected;
sidebarVBox.AddChild(backgroundList);
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/static_lights") });
lightListPanel = new VBoxContainer();
sidebarVBox.AddChild(lightListPanel);
addLightBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/add_light") };
addLightBtn.Pressed += OnAddLight;
sidebarVBox.AddChild(addLightBtn);
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/selected_props") });
var radiusHBox = new HBoxContainer();
radiusHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/radius") });
radiusEdit = new LineEdit { Text = "300" };
radiusEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
radiusEdit.TextChanged += OnRadiusChanged;
radiusHBox.AddChild(radiusEdit);
sidebarVBox.AddChild(radiusHBox);
var infHBox = new HBoxContainer();
infHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/influence") });
influenceEdit = new LineEdit { Text = "1" };
influenceEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
influenceEdit.TextChanged += OnInfluenceChanged;
infHBox.AddChild(influenceEdit);
sidebarVBox.AddChild(infHBox);
var colorHBox = new HBoxContainer();
colorHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/color") });
colorPicker = new ColorPickerButton { Color = Colors.Yellow };
colorPicker.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
colorPicker.ColorChanged += OnColorChanged;
colorHBox.AddChild(colorPicker);
sidebarVBox.AddChild(colorHBox);
var importBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/import_json") };
importBtn.Pressed += OnImport;
sidebarVBox.AddChild(importBtn);
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/norm_coords") });
var previewContainer = new PanelContainer();
previewContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
previewContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
mainHBox.AddChild(previewContainer);
preview = new TextureRect();
preview.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize;
preview.StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered;
preview.MouseFilter = Control.MouseFilterEnum.Stop;
preview.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
preview.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
previewContainer.AddChild(preview);
previewMaterial = controller.CreatePreviewMaterial();
preview.Material = previewMaterial;
jsonEdit = new TextEdit();
jsonEdit.CustomMinimumSize = new Vector2(0, 70);
jsonEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
jsonEdit.WrapMode = TextEdit.LineWrappingMode.Boundary;
jsonEdit.TextChanged += OnJsonEditChanged;
outerVBox.AddChild(jsonEdit);
}
private void OnJsonEditChanged()
{
}
private void UpdateJsonEdit()
{
var json = controller.ExportLightsJson();
if (jsonEdit.Text != json)
jsonEdit.Text = json;
}
private void LoadBackgroundList()
{
bgIds.Clear();
backgroundList.Clear();
var ids = ModResourceService.GetBackgroundIds();
bgIds.AddRange(ids);
foreach (var id in ids)
backgroundList.AddItem(id);
}
private void OnBackgroundSelected(long index)
{
var bgId = bgIds[(int)index];
var tex = BackgroundFactory.Instance.Get(bgId);
preview.Texture = tex;
controller.SetBackgroundForPreview(tex, previewMaterial);
var depthTex = BackgroundFactory.Instance.Get(bgId + "_depth");
if (depthTex != null)
controller.SetDepthForPreview(depthTex, previewMaterial);
var albedoTex = BackgroundFactory.Instance.Get(bgId + "_albedo");
if (albedoTex != null)
controller.SetAlbedoForPreview(albedoTex, previewMaterial);
}
private void OnAddLight()
{
if (handles.Count >= 10)
return;
var light = new LightDef
{
X = 0.4f,
Y = 0.3f,
Radius = 300f,
Influence = 1f,
ColorHex = "#ffff00"
};
controller.AddLight(light);
AddHandle(light, controller.Lights.Count - 1);
RefreshAll();
}
private void AddHandle(LightDef light, int index)
{
var handle = new LightEditorHandle(light, index);
handle.DragUpdate += globalCenter =>
{
var localCenter = globalCenter - preview.GlobalPosition;
var currentSize = PreviewSize();
light.FromPixelPos(localCenter, currentSize);
RefreshAll();
};
handle.Clicked += () => SelectHandle(index);
preview.AddChild(handle);
handles.Add(handle);
RepositionHandle(handle, light);
}
private void RepositionHandle(LightEditorHandle handle, LightDef light)
{
var size = PreviewSize();
handle.GlobalPosition = preview.GlobalPosition
+ new Vector2(light.X * size.X - 12, light.Y * size.Y - 12);
}
private void RepositionHandles()
{
for (int i = 0; i < handles.Count && i < controller.Lights.Count; i++)
RepositionHandle(handles[i], controller.Lights[i]);
}
private void SelectHandle(int index)
{
selectedIndex = index;
if (index < 0 || index >= controller.Lights.Count)
{
radiusEdit.Text = "300";
influenceEdit.Text = "1";
colorPicker.Color = Colors.Yellow;
return;
}
var light = controller.Lights[index];
radiusEdit.Text = light.Radius.ToString("F0");
influenceEdit.Text = light.Influence.ToString("F2");
colorPicker.Color = light.ToColor();
}
private void OnRadiusChanged(string text)
{
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
return;
if (float.TryParse(text, out var val))
{
controller.Lights[selectedIndex].Radius = val;
RefreshAll();
}
}
private void OnInfluenceChanged(string text)
{
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
return;
if (float.TryParse(text, out var val))
{
controller.Lights[selectedIndex].Influence = val;
RefreshAll();
}
}
private void OnColorChanged(Color color)
{
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
return;
controller.Lights[selectedIndex].ColorHex = color.ToHtml();
RefreshAll();
}
private void UpdateLightList()
{
foreach (var child in lightListPanel.GetChildren())
child.QueueFree();
for (int i = 0; i < controller.Lights.Count; i++)
{
var light = controller.Lights[i];
var hBox = new HBoxContainer();
var label = new Label
{
Text = string.Format(TranslationServer.Translate("tools/light_editor/light_n"), i + 1, light.X, light.Y),
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
};
hBox.AddChild(label);
var selBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/btn_select") };
int idx = i;
selBtn.Pressed += () => SelectHandle(idx);
hBox.AddChild(selBtn);
var delBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/btn_delete") };
delBtn.Pressed += () => RemoveLight(idx);
hBox.AddChild(delBtn);
lightListPanel.AddChild(hBox);
}
addLightBtn.Disabled = controller.Lights.Count >= 10;
}
private void RemoveLight(int index)
{
if (index < 0 || index >= controller.Lights.Count)
return;
controller.RemoveLight(index);
if (index < handles.Count)
{
handles[index].QueueFree();
handles.RemoveAt(index);
}
for (int i = 0; i < handles.Count; i++)
handles[i].LightIndex = i;
if (selectedIndex == index)
selectedIndex = -1;
else if (selectedIndex > index)
selectedIndex--;
RefreshAll();
}
private void OnImport()
{
var json = jsonEdit.Text;
if (string.IsNullOrWhiteSpace(json))
return;
try
{
var lights = JsonSerializer.Deserialize>(json, Cthangover.Core.Utils.JsonOptionsProvider.ReadOptions);
if (lights == null || lights.Count == 0)
return;
foreach (var h in handles)
h.QueueFree();
handles.Clear();
controller.Clear();
foreach (var light in lights)
{
controller.AddLight(light);
AddHandle(light, controller.Lights.Count - 1);
}
selectedIndex = -1;
RefreshAll();
}
catch (Exception ex)
{
GameLogger.Log("LIGHT_EDITOR", $"Import JSON failed: {ex.Message}", LogLevel.Error);
}
}
}
}