#if UNITY_EDITOR
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
namespace jeanf.scenemanagement
{
///
/// Edit-mode prediction of the zone would assign to a
/// world position in play mode. Tests against every in the OPEN
/// scenes (open the SubScene holding the volumes for edit, or nothing is found) and runs the
/// exact same failsafe chain through the same :
/// exact → soft (±tolerance) → lifted (pivot above a volume's top) → nearest fallback → none.
/// The chain parameters are read from the scene's bridge instance when one exists, so tuning
/// the bridge component re-tunes the prediction.
///
public static class EditorZoneResolver
{
public readonly struct Resolution
{
public readonly Zone Zone;
public readonly ObjectZoneTrackingBridge.MatchKind Kind;
public readonly VolumeAuthoring Volume;
/// Meters: lift overshoot for Lifted, surface distance for Fallback, 0 otherwise.
public readonly float Distance;
/// Volumes whose tolerance-expanded box contains the point — >1 means the
/// assignment is ambiguous (pivot within tolerance of a neighboring room).
public readonly int AmbiguousMatches;
public bool HasZone => Zone != null;
public Resolution(Zone zone, ObjectZoneTrackingBridge.MatchKind kind, VolumeAuthoring volume,
float distance, int ambiguousMatches)
{
Zone = zone; Kind = kind; Volume = volume; Distance = distance; AmbiguousMatches = ambiguousMatches;
}
}
public readonly struct ChainParams
{
public readonly float Tolerance;
public readonly float MaxLiftAbove;
public readonly float MaxFallbackDistance;
public ChainParams(float tolerance, float maxLiftAbove, float maxFallbackDistance)
{
Tolerance = tolerance; MaxLiftAbove = maxLiftAbove; MaxFallbackDistance = maxFallbackDistance;
}
}
/// Chain parameters of the bridge in the open scenes, or the bridge's serialized defaults.
public static ChainParams SceneChainParams()
{
var bridge = Object.FindAnyObjectByType(FindObjectsInactive.Include);
return bridge != null
? new ChainParams(bridge.CoverageTolerance, bridge.MaxLiftAboveVolume, bridge.MaxFallbackDist)
: new ChainParams(0.25f, 4f, 2f);
}
/// Every volume with a zone in the open scenes (SubScenes must be open for edit).
public static List GatherVolumes()
{
var found = Object.FindObjectsByType(FindObjectsInactive.Include);
var kept = new List(found.Length);
foreach (var volume in found)
if (volume.zone != null) kept.Add(volume);
return kept;
}
/// Same convention as the baker/runtime: orientation and position from the
/// transform, extents from localScale, matrix scale ignored.
private static float4x4 VolumeLocalToWorld(Transform t)
=> float4x4.TRS(t.position, t.rotation, new float3(1f));
public static Resolution Resolve(Vector3 worldPosition, IReadOnlyList volumes, in ChainParams chain)
{
// Ambiguity census first — independent of which pass lands the assignment.
var ambiguous = 0;
for (var v = 0; v < volumes.Count; v++)
{
var t = volumes[v].transform;
if (VolumeMath.ContainsPoint(VolumeLocalToWorld(t), t.localScale, worldPosition, chain.Tolerance))
ambiguous++;
}
// 1) exact containment
for (var v = 0; v < volumes.Count; v++)
{
var t = volumes[v].transform;
if (VolumeMath.ContainsPoint(VolumeLocalToWorld(t), t.localScale, worldPosition))
return new Resolution(volumes[v].zone, ObjectZoneTrackingBridge.MatchKind.Exact, volumes[v], 0f, ambiguous);
}
// 2) epsilon pass
if (chain.Tolerance > 0f)
{
for (var v = 0; v < volumes.Count; v++)
{
var t = volumes[v].transform;
if (VolumeMath.ContainsPoint(VolumeLocalToWorld(t), t.localScale, worldPosition, chain.Tolerance))
return new Resolution(volumes[v].zone, ObjectZoneTrackingBridge.MatchKind.Soft, volumes[v], 0f, ambiguous);
}
}
// 3) lifted pass: smallest overshoot above a volume whose X/Z footprint contains the point
if (chain.MaxLiftAbove > 0f)
{
var best = -1;
var bestOvershoot = chain.MaxLiftAbove;
for (var v = 0; v < volumes.Count; v++)
{
var t = volumes[v].transform;
var overshoot = VolumeMath.LiftAbove(VolumeLocalToWorld(t), t.localScale, worldPosition);
if (overshoot <= 0f || overshoot >= bestOvershoot) continue;
bestOvershoot = overshoot;
best = v;
}
if (best >= 0)
return new Resolution(volumes[best].zone, ObjectZoneTrackingBridge.MatchKind.Lifted, volumes[best], bestOvershoot, ambiguous);
}
// 4) nearest candidate within limit
if (chain.MaxFallbackDistance > 0f)
{
var best = -1;
var bestDistSq = chain.MaxFallbackDistance * chain.MaxFallbackDistance;
for (var v = 0; v < volumes.Count; v++)
{
var t = volumes[v].transform;
var distSq = VolumeMath.DistanceSq(VolumeLocalToWorld(t), t.localScale, worldPosition);
if (distSq >= bestDistSq) continue;
bestDistSq = distSq;
best = v;
}
if (best >= 0)
return new Resolution(volumes[best].zone, ObjectZoneTrackingBridge.MatchKind.Fallback, volumes[best], Mathf.Sqrt(bestDistSq), ambiguous);
}
// 5) pending
return new Resolution(null, ObjectZoneTrackingBridge.MatchKind.None, null, 0f, ambiguous);
}
/// Transforms of every in the open scenes (one
/// entry per GameObject), minus GameObjects carrying an
/// component (position-irrelevant managers).
public static List GatherTrackedObjects()
{
var behaviours = Object.FindObjectsByType(FindObjectsInactive.Include);
var tracked = new HashSet();
var exempt = new HashSet();
var result = new List();
foreach (var behaviour in behaviours)
{
if (behaviour is IZoneValidationExempt) exempt.Add(behaviour.gameObject);
if (behaviour is not IZoneTrackedObject) continue;
if (!tracked.Add(behaviour.gameObject)) continue;
result.Add(behaviour.transform);
}
if (exempt.Count > 0) result.RemoveAll(t => exempt.Contains(t.gameObject));
return result;
}
}
}
#endif