using System; using Cthangover.Core.UI.Theming; using Cthangover.Tools.Common; using Godot; namespace Cthangover.Core.UI.Tool.LightEditor { /// /// Night-time preview of a scene with its static lights. The background is /// drawn through the game's timed_sprite shader (set as this /// control's material), so the preview lights the picture exactly like the /// game; the lights' positions are pushed to the shader in this canvas' /// coordinates and their radii scaled from the 1920-px game width to the /// drawn background width. Radius rings and handles live in a separate /// child overlay — anything drawn by this control itself would be shaded. /// /// A click on a handle selects that light and drags it; a double-click on /// empty background adds a light there. /// public partial class LightEditorCanvas : BackgroundCanvas { private readonly Control _overlay; /// Data being edited; set once by the window. public LightEditorController Controller; /// Preview material (may be null when the shader is missing). public ShaderMaterial PreviewMaterial { get => Material as ShaderMaterial; set => Material = value; } /// Raised when a light moved or was added on the canvas. public event Action Changed; /// Raised when the selection changed on the canvas. public event Action SelectionChanged; public LightEditorCanvas() { _overlay = new Control { MouseFilter = MouseFilterEnum.Ignore }; AddChild(_overlay); _overlay.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _overlay.Connect("draw", Callable.From((Action)DrawRings)); GuiInput += OnGuiInput; } /// One handle per light, coloured like the light. public override void RebuildHandles() { ClearHandles(); if (Controller == null || GetBackgroundRect().Size.X <= 0f) return; for (var i = 0; i < Controller.Lights.Count; i++) { var index = i; var light = Controller.Lights[i]; var handle = AddHandle(new Vector2(light.X, light.Y), light.ToColor(), i, p => { light.X = p.X; light.Y = p.Y; Refresh(); Changed?.Invoke(); }, index == Controller.SelectedIndex ? 20f : 16f); handle.Clicked = () => SelectLight(index); } Refresh(); } /// Redraws the preview and re-uploads the shader uniforms. public void Refresh() { QueueRedraw(); _overlay.QueueRedraw(); } private void SelectLight(int index) { if (index == Controller.SelectedIndex) return; Controller.Select(index); SelectionChanged?.Invoke(); CallDeferred(nameof(RebuildHandles)); } private void OnGuiInput(InputEvent @event) { if (Controller == null || @event is not InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } mb) return; if (mb.DoubleClick && Controller.Add(Clamp01(LocalToNorm(mb.Position))) != null) { RebuildHandles(); Changed?.Invoke(); SelectionChanged?.Invoke(); } AcceptEvent(); } protected override void DrawContent(Rect2 rect) { // The shader compares against world_pos (MODEL_MATRIX * VERTEX), i.e. // canvas coordinates of the window, not this control's local ones. Controller?.UpdatePreview(PreviewMaterial, n => GlobalPosition + NormToLocal(n), rect.Size.X / LightEditorController.GameWidth); } private void DrawRings() { var rect = GetBackgroundRect(); if (Controller == null || rect.Size.X <= 0f) return; var scale = rect.Size.X / LightEditorController.GameWidth; for (var i = 0; i < Controller.Lights.Count; i++) { var light = Controller.Lights[i]; var selected = i == Controller.SelectedIndex; var centre = NormToLocal(new Vector2(light.X, light.Y)); var color = selected ? UiTokens.AccentBright : UiTokens.Alpha(light.ToColor(), 0.45f); _overlay.DrawArc(centre, light.Radius * scale, 0f, Mathf.Tau, 64, color, selected ? 1.5f : 1f, true); } } } }