diff --git a/Assets/Scripts/Components/PictureFrameBuilder.cs b/Assets/Scripts/Components/PictureFrameBuilder.cs index 13059a7..62fc400 100644 --- a/Assets/Scripts/Components/PictureFrameBuilder.cs +++ b/Assets/Scripts/Components/PictureFrameBuilder.cs @@ -13,6 +13,10 @@ namespace Chameleon.Components // The generated mesh is a per-instance HideFlags.DontSave object (never saved into the scene file); // it is rebuilt on scene load / domain reload via OnEnable. Duplicated frames get their own mesh // (identified by instance id), so two frames never write into one shared mesh. + // + // GenerateColliders maintains matching BoxColliders (corner radius ignored): 4 border bars, a slab + // behind the inner floor, plus a plinth box when the picture is raised. Only colliders tracked in + // _colliders are touched - manually added ones are left alone. [ExecuteAlways] [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public sealed class PictureFrameBuilder : MonoBehaviour @@ -52,6 +56,12 @@ namespace Chameleon.Components [SerializeField] Material _frameMaterial; [SerializeField] Material _backMaterial; + [Header("Colliders")] + [Tooltip("Automatically maintain BoxColliders matching the frame (corner radius is ignored).")] + [SerializeField] bool _generateColliders = true; + // Ownership list so rebuilds never touch colliders the user added by hand. + [SerializeField, HideInInspector] List _colliders = new List(); + readonly List _verts = new List(); readonly List _normals = new List(); readonly List _uvs = new List(); @@ -62,23 +72,31 @@ namespace Chameleon.Components Mesh _mesh; bool _rebuildQueued; + bool _rebuilding; void OnEnable() => Rebuild(); + // Mesh/renderer writes are not allowed during OnValidate, so it only flags; the actual + // rebuild runs in Update (ExecuteAlways covers edit mode). void OnValidate() { -#if UNITY_EDITOR - // Mesh/renderer writes are not allowed during OnValidate; defer one editor tick. - if (_rebuildQueued) return; _rebuildQueued = true; - UnityEditor.EditorApplication.delayCall += () => - { - _rebuildQueued = false; - if (this && isActiveAndEnabled) Rebuild(); - }; +#if UNITY_EDITOR + // In edit mode Update only ticks when the editor decides to pump the player loop; + // request one so the last inspector change never stays un-applied. + UnityEditor.EditorApplication.QueuePlayerLoopUpdate(); #endif } + void Update() + { + if (!_rebuildQueued || _rebuilding) return; + _rebuildQueued = false; + _rebuilding = true; + try { Rebuild(); } + finally { _rebuilding = false; } + } + void OnDestroy() { if (!_mesh) return; @@ -150,6 +168,49 @@ namespace Chameleon.Components _mesh.RecalculateTangents(); ApplyMaterials(); + RebuildColliders(w, h, t, d, ow, oh, pw, ph, id, pz); + } + + // Box approximation, corner radius intentionally ignored: 4 border bars (full depth), one slab + // filling the opening from the inner floor to the back, and a plinth box only when the picture + // is raised out of the floor. A sunken picture recess is NOT carved out - the slab stays full + // (colliders would need a per-side split for a cosmetic dent). + void RebuildColliders(float w, float h, float t, float d, float ow, float oh, float pw, float ph, float id, float pz) + { + _colliders.RemoveAll(c => !c); + int count = 0; + if (_generateColliders) + { + SetBox(count++, new Vector3(0f, (h - t) * 0.5f, d * 0.5f), new Vector3(w, t, d)); + SetBox(count++, new Vector3(0f, -(h - t) * 0.5f, d * 0.5f), new Vector3(w, t, d)); + SetBox(count++, new Vector3((w - t) * 0.5f, 0f, d * 0.5f), new Vector3(t, oh, d)); + SetBox(count++, new Vector3(-(w - t) * 0.5f, 0f, d * 0.5f), new Vector3(t, oh, d)); + + // Keep a minimal slab even when the inner floor sits on the back plate, so the opening + // is never backless. + float slabFront = Mathf.Min(id, d - 0.002f); + SetBox(count++, new Vector3(0f, 0f, (slabFront + d) * 0.5f), new Vector3(ow, oh, d - slabFront)); + + if (pz < id - 1e-4f) + SetBox(count++, new Vector3(0f, 0f, (pz + id) * 0.5f), new Vector3(pw, ph, id - pz)); + } + + for (int i = _colliders.Count - 1; i >= count; i--) + { + var extra = _colliders[i]; + _colliders.RemoveAt(i); + if (!extra) continue; + if (Application.isPlaying) Destroy(extra); + else DestroyImmediate(extra); + } + } + + void SetBox(int index, Vector3 center, Vector3 size) + { + while (_colliders.Count <= index) _colliders.Add(gameObject.AddComponent()); + var box = _colliders[index]; + if (box.center != center) box.center = center; + if (box.size != size) box.size = size; } void EnsureMesh(MeshFilter filter)