OMRVisualFoundation.EdgeCues.cs
using Sandbox;
using System;

/// <summary>
/// Optional screen-space geometry separation for the gameplay world.
///
/// Phase 8F3E1 Hotfix 2 uses S&box's native BasePostProcess pipeline rather
/// than a raw camera CommandList. OMRVisualFoundation remains the authored
/// source of truth; a runtime-only OMREdgeCuePostProcess mirrors these values
/// onto the gameplay/world camera while the scene is playing.
/// </summary>
public sealed partial class OMRVisualFoundation
{
    public enum EdgeCueDebugMode
    {
        Off = 0,
        Depth = 1,
        Normals = 2,
        Combined = 3,
        PassCheck = 4,
        RawDepth = 5,
        RawNormals = 6
    }

    [Property, Group( "World Geometry/Edge Cues" ), Title( "Enable Edge Cues" )]
    [Description( "Adds subtle world-only separation from scene depth and G-buffer normals. Viewmodel, physical scope surfaces and HUD are excluded by render order." )]
    public bool EnableEdgeCues { get; set; } = false;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 1f ), Title( "Overall Strength" )]
    public float EdgeCueStrength { get; set; } = 1f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 2f ), Title( "Depth Strength" )]
    public float EdgeCueDepthStrength { get; set; } = 2f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 2f ), Title( "Normal Strength" )]
    public float EdgeCueNormalStrength { get; set; } = 1f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0.001f, 0.25f ), Title( "Depth Threshold" )]
    [Description( "Relative view-depth discontinuity required before an edge contributes. Lower values detect smaller depth changes." )]
    public float EdgeCueDepthThreshold { get; set; } = 0.15f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0.001f, 1f ), Title( "Normal Threshold" )]
    [Description( "World-normal discontinuity required before an edge contributes. Lower values react to shallower corners." )]
    public float EdgeCueNormalThreshold { get; set; } = 0.11f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 1f, 3f ), Title( "Sample Radius (px)" )]
    [Description( "Distance in screen pixels used by the four-neighbour edge detector. Start at 1; larger values produce broader, less subtle separation." )]
    public float EdgeCueSampleRadius { get; set; } = 1.4f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 4096f ), Title( "Distance Fade Start" )]
    public float EdgeCueFadeStart { get; set; } = 1000f;

    [Property, Group( "World Geometry/Edge Cues" ), Range( 1f, 4096f ), Title( "Distance Fade End" )]
    [Description( "Edge cues fade away with view distance so haze/fog can still hide distant geometry instead of being defeated by a screen-space outline." )]
    public float EdgeCueFadeEnd { get; set; } = 1500f;

    [Property, Group( "World Geometry/Edge Cues" ), Title( "Debug View" )]
    [Description( "Pass Check must turn the world magenta. Raw Depth and Raw Normals expose the renderer inputs directly. Depth/Normals/Combined show the detected edges." )]
    public EdgeCueDebugMode EdgeCueDebug { get; set; } = EdgeCueDebugMode.Off;

    [Button, Group( "World Geometry/Edge Cues" ), Title( "Reset Edge Cues" )]
    public void ResetEdgeCues()
    {
        EnableEdgeCues = false;
        EdgeCueStrength = 1f;
        EdgeCueDepthStrength = 2f;
        EdgeCueNormalStrength = 1f;
        EdgeCueDepthThreshold = 0.15f;
        EdgeCueNormalThreshold = 0.11f;
        EdgeCueSampleRadius = 1.4f;
        EdgeCueFadeStart = 1000f;
        EdgeCueFadeEnd = 1500f;
        EdgeCueDebug = EdgeCueDebugMode.Off;

        RebuildEdgeCueCommands();
    }

    private OMREdgeCuePostProcess _edgeCuePostProcess;
    private bool _edgeCuePostProcessCreatedByFoundation;
    private bool _edgeCueReadyLogged;

    /// <summary>
    /// Kept under the original method name so OMRVisualFoundation.cs does not
    /// need another lifecycle refactor. It now synchronizes a native
    /// BasePostProcess component instead of rebuilding a raw CommandList.
    /// </summary>
    private void RebuildEdgeCueCommands()
    {
        // OMRVisualFoundation executes in editor. Do not mutate the authored
        // scene by creating an internal helper component outside Play mode.
        if ( !Game.IsPlaying )
        {
            DisableRuntimeEdgeCuePostProcess();
            return;
        }

        CameraComponent camera = GetComponent<CameraComponent>( true );
        if ( camera is null || !camera.IsValid )
            return;

        if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
        {
            _edgeCuePostProcess = GetComponent<OMREdgeCuePostProcess>( true );

            if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
            {
                _edgeCuePostProcess = GameObject.AddComponent<OMREdgeCuePostProcess>( false );
                _edgeCuePostProcessCreatedByFoundation = true;
            }
        }

        if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
            return;

        float fadeStart = MathF.Max( 0f, EdgeCueFadeStart );
        float fadeEnd = MathF.Max( fadeStart + 1f, EdgeCueFadeEnd );

        _edgeCuePostProcess.Strength = MathX.Clamp( EdgeCueStrength, 0f, 1f );
        _edgeCuePostProcess.DepthStrength = MathX.Clamp( EdgeCueDepthStrength, 0f, 2f );
        _edgeCuePostProcess.NormalStrength = MathX.Clamp( EdgeCueNormalStrength, 0f, 2f );
        _edgeCuePostProcess.DepthThreshold = MathX.Clamp( EdgeCueDepthThreshold, 0.001f, 0.25f );
        _edgeCuePostProcess.NormalThreshold = MathX.Clamp( EdgeCueNormalThreshold, 0.001f, 1f );
        _edgeCuePostProcess.SampleRadius = MathX.Clamp( EdgeCueSampleRadius, 1f, 3f );
        _edgeCuePostProcess.FadeStart = fadeStart;
        _edgeCuePostProcess.FadeEnd = fadeEnd;
        _edgeCuePostProcess.DebugMode = (int)EdgeCueDebug;

        bool debugActive = EdgeCueDebug != EdgeCueDebugMode.Off;
        bool shouldRun = EnableEdgeCues &&
            (_edgeCuePostProcess.Strength > 0.0001f || debugActive);

        _edgeCuePostProcess.Enabled = shouldRun;

        if ( shouldRun && LogSetup && !_edgeCueReadyLogged )
        {
            _edgeCueReadyLogged = true;
            Log.Info(
                $"[OMR VISUAL] Edge cues using native post-process on '{GameObject.Name}' | " +
                $"CameraPostProcessing:{camera.EnablePostProcessing} | Debug:{EdgeCueDebug}"
            );
        }

        if ( !shouldRun )
            _edgeCueReadyLogged = false;
    }

    private void DisableRuntimeEdgeCuePostProcess()
    {
        if ( _edgeCuePostProcess is not null && _edgeCuePostProcess.IsValid )
            _edgeCuePostProcess.Enabled = false;

        _edgeCueReadyLogged = false;
    }

    private void ReleaseEdgeCuePass()
    {
        if ( _edgeCuePostProcess is not null && _edgeCuePostProcess.IsValid )
        {
            _edgeCuePostProcess.Enabled = false;

            // Only destroy helpers that this foundation created at runtime.
            // An authored component, should one ever be added manually, is left alone.
            if ( _edgeCuePostProcessCreatedByFoundation )
                _edgeCuePostProcess.Destroy();
        }

        _edgeCuePostProcess = null;
        _edgeCuePostProcessCreatedByFoundation = false;
        _edgeCueReadyLogged = false;
    }
}