using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; // Les trous posés sur le comptoir, pour un panneau. // // Il n'y a qu'un comptoir, donc qu'un jeu de trous : ils sont retenus une seule // fois, en pouces, et chaque panneau qui porte un HoleBoard les redessine chez // lui. Un évier posé dans Panel Holes apparaît donc aussi dans le récapitulatif, // sans rien avoir à recopier. // // À poser à côté du CreateShape du panneau. [DisallowMultipleComponent] public class HoleBoard : MonoBehaviour { // Un trou tel qu'il est retenu, indépendamment de tout panneau. private class Placed { public Hole.Kind kind; public Vector2 sizeInches; public float cornerInches; public Vector2 positionInches; } // Le comptoir est unique, ses trous aussi. private static readonly List Holes = new(); private static readonly List Boards = new(); [Header("Références (vide = trouvées automatiquement)")] [Tooltip("Le comptoir sur lequel les trous sont posés. Vide = le CreateShape de cet objet.")] [SerializeField] private CreateShape shape; [Tooltip("Où les trous sont créés. Vide = cet objet.")] [SerializeField] private RectTransform container; [Header("Apparence")] [Tooltip("Le vide du trou. En général la couleur du fond du panneau.")] [SerializeField] private Color fillColor = new Color(0.8862745f, 0.8666667f, 0.8666667f); // E2DDDD [SerializeField] private Color outlineColor = Color.black; [SerializeField] private float outlineThickness = 2f; [Header("Édition")] [Tooltip("Décocher sur un panneau qui montre les trous sans permettre de les choisir ni de les déplacer, comme la vérification.")] [SerializeField] private bool editable = true; [Header("Trou choisi")] [Tooltip("Épaisseur du trait d'un trou choisi. Sa couleur est celle de la marque, _G.Color1.")] [SerializeField] private float selectedThickness = 4f; [Tooltip("Le bouton de suppression, posé au coin haut-droit du trou choisi. Masqué tant qu'aucun trou n'est choisi.")] [SerializeField] private Button deleteButton; [Tooltip("Le bouton des dimensions, posé sous le trou choisi. Son texte affiche 30 x 18.")] [SerializeField] private Button sizeButton; [Tooltip("Le pavé à deux mesures ouvert par le bouton des dimensions, KEYBOARDNUMBERDOUBLE. Vide = dimensions affichées mais non modifiables.")] [SerializeField] private KeyNumberDouble keyboard; [Tooltip("Écart des deux boutons hors du trou, en pixels.")] [SerializeField] private float selectionOffset = 12f; // Le trou choisi, désigné par son rang : les vues sont refaites à chaque // changement, une référence directe ne survivrait pas à la reconstruction. private static int Chosen = -1; // Vrai tant qu'un trou est traîné. Les deux boutons s'effacent alors : ils // resteraient plantés là où le trou était, et gêneraient le geste. private static bool Dragging; // Les trous dessinés ici, dans l'ordre de la liste commune. private readonly List _views = new(); private TMP_Text _sizeLabel; private Hole Selected => Chosen >= 0 && Chosen < _views.Count ? _views[Chosen] : null; public bool Editable => editable; public float Scale => shape != null ? shape.Scale : 1f; public Vector2 CenterInches => shape != null ? shape.CenterInches : Vector2.zero; public Vector2 InchesToLocal(Vector2 inches) { return shape != null ? shape.InchesToLocal(inches) : Vector2.zero; } void Awake() { if (shape == null) shape = GetComponent(); if (container == null) container = (RectTransform)transform; if (sizeButton != null) _sizeLabel = sizeButton.GetComponentInChildren(true); } void OnEnable() { Boards.Add(this); // Le comptoir peut changer de taille sans que les trous bougent en pouces : // c'est leur place en pixels qui est à refaire. if (shape != null) shape.onBuilt += Replace; if (deleteButton != null) deleteButton.onClick.AddListener(DeleteSelected); if (sizeButton != null) sizeButton.onClick.AddListener(EditSize); // Un panneau qui s'ouvre repart sans trou choisi : ses deux boutons // pendraient sinon à côté d'un trou que plus personne ne regarde. Chosen = -1; Rebuild(); } void OnDisable() { Boards.Remove(this); if (shape != null) shape.onBuilt -= Replace; if (deleteButton != null) deleteButton.onClick.RemoveListener(DeleteSelected); if (sizeButton != null) sizeButton.onClick.RemoveListener(EditSize); } // Où le doigt se trouve sur le comptoir, en pouces. public Vector2 PointerToInches(PointerEventData eventData) { if (shape == null) return Vector2.zero; RectTransformUtility.ScreenPointToWorldPointInRectangle( (RectTransform)shape.transform, eventData.position, eventData.pressEventCamera, out Vector3 world); return shape.WorldToInches(world); } // Pose un trou et rend celui de ce panneau, pour que le bouton qui l'a créé // puisse le faire suivre le doigt. public Hole Add(Hole.Kind kind, Vector2 sizeInches, float cornerInches, Vector2 positionInches) { Holes.Add(new Placed { kind = kind, sizeInches = sizeInches, cornerInches = cornerInches, positionInches = positionInches, }); RebuildAll(); return _views.Count > 0 ? _views[_views.Count - 1] : null; } // Le trou est lâché : il reste s'il tient sur le comptoir, il est retiré sinon. // // Une seule règle, et elle donne les deux gestes attendus : glisser hors du // comptoir annule une pose commencée par mégarde, et sort un trou déjà posé // dont on ne veut plus. Sans quoi rien ne permettrait d'en supprimer un. // Un trou est saisi : les deux boutons s'effacent le temps du geste. public void BeginDrag() { Dragging = true; RefreshSelectionEverywhere(); } public void Drop(Hole hole) { Dragging = false; if (hole == null) { RefreshSelectionEverywhere(); return; } if (!Fits(hole)) { Remove(hole); return; } Save(hole); // Le trou qu'on vient de poser est celui qu'on veut régler. Select(hole); } // Choisit un trou : son contour prend la couleur de marque et ses deux boutons // paraissent. Choisir ailleurs, ou nulle part, libère le précédent. public void Select(Hole hole) { Chosen = hole != null ? _views.IndexOf(hole) : -1; RefreshSelectionEverywhere(); } // À brancher sur le OnClick du fond du panneau, pour lâcher le trou choisi. public void ClearSelection() { Chosen = -1; RefreshSelectionEverywhere(); } // À brancher sur le bouton de suppression, ou appelable directement. public void DeleteSelected() { Remove(Selected); } // Le bouton des dimensions ouvre le pavé à deux mesures sur la largeur et la // profondeur du trou choisi. public void EditSize() { if (Selected == null || keyboard == null) return; Vector2 size = Selected.SizeInches; // Un pavé rouvert sans validation laisserait l'abonnement en place : on // repart toujours d'un seul. keyboard.onValidated.RemoveListener(OnSizeValidated); keyboard.onValidated.AddListener(OnSizeValidated); keyboard.Open(this, size.x, size.y); } private void OnSizeValidated(Vector2 size, string direction) { // Le pavé est partagé : chacun ne répond que de sa propre ouverture. if ((Object)keyboard.Caller != this) return; keyboard.onValidated.RemoveListener(OnSizeValidated); Resize(size); } // Un trou n'est pas redimensionné sur un seul panneau : c'est le même comptoir // partout, et le récapitulatif doit montrer l'évier qui vient d'être réglé. private void Resize(Vector2 sizeInches) { if (Chosen < 0 || Chosen >= Holes.Count) return; Holes[Chosen].sizeInches = new Vector2(Mathf.Max(0f, sizeInches.x), Mathf.Max(0f, sizeInches.y)); RebuildAll(); } public void Remove(Hole hole) { int index = hole != null ? _views.IndexOf(hole) : -1; if (index < 0 || index >= Holes.Count) return; Holes.RemoveAt(index); // Les rangs suivants remontent d'un cran : sans cela le trou choisi ne // serait plus celui que l'on croit. if (Chosen == index) Chosen = -1; else if (Chosen > index) Chosen--; RebuildAll(); } [ContextMenu("Retirer tous les trous")] public void RemoveAll() { Holes.Clear(); Chosen = -1; RebuildAll(); } // Un trou tient quand ses quatre coins tombent sur le comptoir. Le test porte // sur le cadre, pas sur le contour arrondi : un évier rond passe donc d'un // cheveu plus au large, ce qui va dans le sens de la prudence. private bool Fits(Hole hole) { if (shape == null) return false; foreach (Vector2 corner in hole.CornersInches()) { if (!shape.ContainsInches(corner)) return false; } return true; } // Recopie la position du trou déplacé dans la liste commune, puis la porte aux // autres panneaux. private void Save(Hole hole) { int index = _views.IndexOf(hole); if (index < 0 || index >= Holes.Count) return; Holes[index].positionInches = hole.PositionInches; foreach (HoleBoard board in Boards) { if (board != this) board.Replace(); } } private static void RebuildAll() { foreach (HoleBoard board in Boards) board.Rebuild(); } // Refait les trous de ce panneau d'après la liste commune. public void Rebuild() { foreach (Hole view in _views) { if (view == null) continue; if (Application.isPlaying) Destroy(view.gameObject); else DestroyImmediate(view.gameObject); } _views.Clear(); if (container == null) return; foreach (Placed placed in Holes) { GameObject child = new GameObject("Hole " + (_views.Count + 1), typeof(RectTransform)); child.layer = gameObject.layer; ((RectTransform)child.transform).SetParent(container, false); Hole hole = child.AddComponent(); hole.Setup(this, placed.kind, placed.sizeInches, placed.cornerInches, fillColor, outlineColor, outlineThickness); // Seul un panneau qui laisse déplacer ses trous les fait cliquables. hole.raycastTarget = editable; hole.PositionInches = placed.positionInches; _views.Add(hole); } RefreshSelection(); } // Le comptoir a changé de taille : les trous gardent leurs pouces, pas leurs // pixels. private void Replace() { for (int i = 0; i < _views.Count && i < Holes.Count; i++) { if (_views[i] == null) continue; _views[i].PositionInches = Holes[i].positionInches; } RefreshSelection(); } // Le trou choisi se distingue des autres, et ses deux boutons se posent à ses // bords. Un panneau qui n'est pas modifiable teinte sans rien afficher. private void RefreshSelection() { Color chosenColor = BrandColor(); // Un panneau qui ne fait que montrer les trous n'en distingue aucun : le // trou choisi dans Panel Holes ne doit pas ressortir dans le récapitulatif. for (int i = 0; i < _views.Count; i++) { if (_views[i] != null) _views[i].SetSelected(editable && i == Chosen, chosenColor, selectedThickness); } Hole selected = editable && !Dragging ? Selected : null; if (deleteButton != null) deleteButton.gameObject.SetActive(selected != null); if (sizeButton != null) sizeButton.gameObject.SetActive(selected != null); if (selected == null || shape == null) return; Vector2 half = selected.SizeInches * 0.5f; Vector2 center = selected.PositionInches; Place(deleteButton, center + half, new Vector2(selectionOffset, selectionOffset)); Place(sizeButton, center + new Vector2(0f, -half.y), new Vector2(0f, -selectionOffset)); if (_sizeLabel != null) _sizeLabel.text = SizeText(selected); } // Les deux boutons vivent où l'on veut dans le panneau : ils sont posés en // coordonnées monde, comme CreateShape le fait pour les siens. private void Place(Button button, Vector2 inches, Vector2 pixelOffset) { if (button == null) return; button.transform.position = shape.LocalToWorld(shape.InchesToLocal(inches) + pixelOffset); } private static string SizeText(Hole hole) { return KeyNumber.Format(hole.SizeInches.x) + _G.Sys + " x " + KeyNumber.Format(hole.SizeInches.y) + _G.Sys; } // La couleur de marque est tenue en hexadécimal dans _G, et posée au chargement // selon l'enseigne : on la relit à chaque fois plutôt que de la retenir. private Color BrandColor() { if (ColorUtility.TryParseHtmlString("#" + _G.Color1, out Color color)) return color; return outlineColor; } private static void RefreshSelectionEverywhere() { foreach (HoleBoard board in Boards) board.RefreshSelection(); } }