using System.Linq;
using UnityEditor;
using UnityEngine;
namespace jeanf.scenemanagement.editor
{
///
/// Live view of the zone-tracking state: per-zone object lists, pending (coverage-gap)
/// objects, and counts. Select the bridge in play mode to watch assignments happen.
///
[CustomEditor(typeof(ObjectZoneTrackingBridge))]
public class ObjectZoneTrackingBridgeEditor : UnityEditor.Editor
{
private const string SimulateKey = "OZTB_simulateProximity";
private const string SimPosKey = "OZTB_simPlayerPos";
private bool _showPending = true;
private string _zoneFilter = "";
public override bool RequiresConstantRepaint() => Application.isPlaying;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Live tracking", EditorStyles.boldLabel);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Enter Play mode to see live zone assignments here.", MessageType.Info);
DrawProximitySimulationControls();
return;
}
var bridge = (ObjectZoneTrackingBridge)target;
var zoneDict = WorldManager.GetZoneDictionary();
EditorGUILayout.LabelField($"Registered objects: {bridge.TrackedObjectCount}");
var pending = bridge.PendingObjects.Where(g => g != null).ToList();
_showPending = EditorGUILayout.Foldout(_showPending, $"Pending / no volume match ({pending.Count})", true);
if (_showPending)
{
if (pending.Count == 0) EditorGUILayout.LabelField(" none — full coverage");
foreach (var go in pending)
using (new EditorGUI.DisabledScope(true))
EditorGUILayout.ObjectField(go, typeof(GameObject), true);
}
EditorGUILayout.Space(4);
_zoneFilter = EditorGUILayout.TextField("Filter zones", _zoneFilter);
foreach (var zoneId in bridge.ZoneIdsWithObjects.OrderBy(z => z))
{
var label = zoneId;
if (zoneDict != null && zoneDict.TryGetValue(zoneId, out var zone) && zone != null && !string.IsNullOrEmpty(zone.zoneName))
label = $"{zone.zoneName} ({zoneId})";
if (!string.IsNullOrEmpty(_zoneFilter) &&
label.IndexOf(_zoneFilter, System.StringComparison.OrdinalIgnoreCase) < 0) continue;
var objects = bridge.GetObjectsInZone(zoneId);
var key = $"OZTB_zone_{zoneId}";
var expanded = SessionState.GetBool(key, false);
var next = EditorGUILayout.Foldout(expanded, $"{label} — {objects.Count} object(s)", true);
if (next != expanded) SessionState.SetBool(key, next);
if (!next) continue;
foreach (var go in objects)
using (new EditorGUI.DisabledScope(true))
EditorGUILayout.ObjectField(go, typeof(GameObject), true);
}
}
#region Edit-mode proximity simulation
private static bool SimulateProximity
{
get => SessionState.GetBool(SimulateKey, false);
set => SessionState.SetBool(SimulateKey, value);
}
private static Vector3 SimPlayerPos
{
get => new Vector3(SessionState.GetFloat(SimPosKey + "x", 0f),
SessionState.GetFloat(SimPosKey + "y", 0f),
SessionState.GetFloat(SimPosKey + "z", 0f));
set
{
SessionState.SetFloat(SimPosKey + "x", value.x);
SessionState.SetFloat(SimPosKey + "y", value.y);
SessionState.SetFloat(SimPosKey + "z", value.z);
}
}
private void DrawProximitySimulationControls()
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Proximity simulation (edit mode)", EditorStyles.boldLabel);
var simulate = EditorGUILayout.ToggleLeft(
new GUIContent("Simulate proximity in Scene view",
"Draws each proximity-gated object's threshold disc and a draggable 'simulated player' " +
"handle in the Scene view. Distance is horizontal-only, exactly like the runtime check — " +
"tune thresholds in the list above and watch discs and states update live."),
SimulateProximity);
if (simulate != SimulateProximity)
{
SimulateProximity = simulate;
if (simulate) SimPlayerPos = SceneView.lastActiveSceneView != null ? SceneView.lastActiveSceneView.pivot : Vector3.zero;
SceneView.RepaintAll();
}
if (simulate && GUILayout.Button("Move simulated player to Scene view pivot"))
{
if (SceneView.lastActiveSceneView != null) SimPlayerPos = SceneView.lastActiveSceneView.pivot;
SceneView.RepaintAll();
}
}
private void OnSceneGUI()
{
if (Application.isPlaying || !SimulateProximity) return;
var bridge = (ObjectZoneTrackingBridge)target;
var proximityByZone = new System.Collections.Generic.Dictionary();
foreach (var config in bridge.ProximityZones)
if (config.zone != null && config.threshold > 0f) proximityByZone[$"{config.zone.id}"] = config.threshold;
if (proximityByZone.Count == 0)
{
Handles.Label(bridge.transform.position, "No proximity zones configured on the bridge.");
return;
}
// Draggable simulated player.
var simPos = SimPlayerPos;
EditorGUI.BeginChangeCheck();
simPos = Handles.PositionHandle(simPos, Quaternion.identity);
if (EditorGUI.EndChangeCheck()) SimPlayerPos = simPos;
Handles.color = new Color(0.3f, 0.7f, 1f, 0.9f);
Handles.SphereHandleCap(0, simPos, Quaternion.identity, 0.25f, EventType.Repaint);
Handles.Label(simPos + Vector3.up * 0.4f, "Simulated player");
var volumes = EditorZoneResolver.GatherVolumes();
if (volumes.Count == 0)
{
Handles.Label(simPos + Vector3.up * 0.7f, "No volumes in open scenes — open the volume SubScene for edit.");
return;
}
var chain = EditorZoneResolver.SceneChainParams();
foreach (var tracked in EditorZoneResolver.GatherTrackedObjects())
{
var resolution = EditorZoneResolver.Resolve(tracked.position, volumes, chain);
if (!resolution.HasZone) continue;
if (!proximityByZone.TryGetValue($"{resolution.Zone.id}", out var threshold)) continue;
// Horizontal distance, mirroring UpdateProximity.
var delta = tracked.position - simPos;
delta.y = 0f;
var distance = delta.magnitude;
var visible = distance <= threshold;
Handles.color = visible ? new Color(0.2f, 0.9f, 0.3f, 0.9f) : new Color(0.95f, 0.4f, 0.25f, 0.7f);
Handles.DrawWireDisc(tracked.position, Vector3.up, threshold, visible ? 3f : 1.5f);
Handles.Label(tracked.position + Vector3.up * 0.3f,
$"{tracked.name}\n{distance:F1}m / {threshold:F1}m — {(visible ? "VISIBLE" : "hidden")}");
}
}
#endregion
}
}