using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; // Un trou posé sur le comptoir : évier, robinet, passe-fil. // // Il se dessine lui-même — le vide, cerné d'un trait — et se déplace au doigt. Sa // taille et sa position sont en pouces, jamais en pixels : le trou garde donc sa // place et ses proportions quand le comptoir change de dimensions. // // Posé et réglé par HoleBoard, il n'y a rien à monter à la main. [RequireComponent(typeof(CanvasRenderer))] public class Hole : MaskableGraphic, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler { public enum Kind { Rectangle, // évier rectangulaire, prise RoundedRectangle, // évier rectangulaire à coins arrondis Round, // évier rond, passe-fil } // Points par quart de tour sur un arrondi. Huit suffisent à l'œil et gardent // le maillage léger. private const int ArcSteps = 8; [SerializeField, HideInInspector] private Kind kind; [SerializeField, HideInInspector] private Vector2 sizeInches = new Vector2(30f, 18f); [SerializeField, HideInInspector] private float cornerInches; [SerializeField, HideInInspector] private Vector2 positionInches; [SerializeField, HideInInspector] private Color outlineColor = Color.black; [SerializeField, HideInInspector] private float outlineThickness = 1f; private HoleBoard _board; // Rayon des coins en pixels, retenu au placement : le maillage est construit // en pixels et n'a pas à retrouver l'échelle du comptoir. private float _cornerPixels; // Écart entre le point saisi et le centre du trou : sans lui, le trou sauterait // sous le doigt au premier mouvement. private Vector2 _grabInches; // Le trait quand le trou n'est pas choisi, pour le lui rendre. private Color _restingOutline = Color.black; private float _restingThickness = 1f; public Kind Contour => kind; public Vector2 SizeInches => sizeInches; public float CornerInches => cornerInches; public Vector2 PositionInches { get => positionInches; set { positionInches = value; Place(); } } // Appelé par HoleBoard juste après la création. public void Setup(HoleBoard board, Kind contour, Vector2 size, float corner, Color fill, Color outline, float thickness) { _board = board; kind = contour; sizeInches = size; cornerInches = corner; color = fill; outlineColor = outline; outlineThickness = thickness; _restingOutline = outline; _restingThickness = thickness; } // Le trou choisi porte la couleur de marque, et un trait plus épais pour se // distinguer des autres d'un coup d'œil. public void SetSelected(bool selected, Color selectedColor, float selectedThickness) { outlineColor = selected ? selectedColor : _restingOutline; outlineThickness = selected ? selectedThickness : _restingThickness; SetVerticesDirty(); } // Le trou reprend sa taille et sa place en pixels d'après le comptoir. public void Place() { if (_board == null) return; float scale = _board.Scale; _cornerPixels = cornerInches * scale; rectTransform.anchorMin = rectTransform.anchorMax = rectTransform.pivot = new Vector2(0.5f, 0.5f); rectTransform.sizeDelta = sizeInches * scale; rectTransform.anchoredPosition = _board.InchesToLocal(positionInches); rectTransform.localRotation = Quaternion.identity; SetVerticesDirty(); } // Les quatre coins du cadre du trou, en pouces. C'est ce qui doit tenir sur le // comptoir : un évier à moitié dans le vide n'est pas un évier posé. public IEnumerable CornersInches() { Vector2 half = sizeInches * 0.5f; yield return positionInches + new Vector2(half.x, half.y); yield return positionInches + new Vector2(-half.x, half.y); yield return positionInches + new Vector2(-half.x, -half.y); yield return positionInches + new Vector2(half.x, -half.y); } // Un clic choisit le trou : son contour change et ses deux boutons paraissent. // // Un glisser ne passe pas par ici : l'Event System n'envoie plus de clic dès // que le doigt a bougé assez pour traîner. C'est le lâcher qui choisit alors le // trou, dans HoleBoard.Drop. public void OnPointerClick(PointerEventData eventData) { if (_board == null || !_board.Editable) return; _board.Select(this); } public void OnBeginDrag(PointerEventData eventData) { if (_board == null || !_board.Editable) return; _grabInches = positionInches - _board.PointerToInches(eventData); _board.BeginDrag(); } public void OnDrag(PointerEventData eventData) { if (_board == null || !_board.Editable) return; PositionInches = _board.PointerToInches(eventData) + _grabInches; } public void OnEndDrag(PointerEventData eventData) { if (_board == null || !_board.Editable) return; _board.Drop(this); } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); List outline = Outline(); if (outline.Count < 3) return; // Le vide du trou : un éventail depuis le centre suffit, ces contours sont // tous convexes. AddVertex(vh, Vector2.zero, color); foreach (Vector2 point in outline) AddVertex(vh, point, color); for (int i = 0; i < outline.Count; i++) { vh.AddTriangle(0, i + 1, (i + 1) % outline.Count + 1); } AddOutline(vh, outline); } // Le trait du contour : un rectangle par segment, posé vers l'intérieur. Chaque // segment déborde d'une demi-épaisseur à ses deux bouts, sinon les angles // resteraient ouverts — le même procédé que les rebords du comptoir. private void AddOutline(VertexHelper vh, List outline) { if (outlineThickness <= 0f || outlineColor.a <= 0f) return; for (int i = 0; i < outline.Count; i++) { Vector2 a = outline[i]; Vector2 b = outline[(i + 1) % outline.Count]; Vector2 along = b - a; if (along.sqrMagnitude < 0.0001f) continue; along.Normalize(); // Vers l'intérieur : la normale doit se rapprocher du centre du trou. Vector2 inward = new Vector2(-along.y, along.x); if (Vector2.Dot(inward, -(a + b) * 0.5f) < 0f) inward = -inward; Vector2 overshoot = along * (outlineThickness * 0.5f); int start = vh.currentVertCount; AddVertex(vh, a - overshoot, outlineColor); AddVertex(vh, b + overshoot, outlineColor); AddVertex(vh, b + overshoot + inward * outlineThickness, outlineColor); AddVertex(vh, a - overshoot + inward * outlineThickness, outlineColor); vh.AddTriangle(start, start + 1, start + 2); vh.AddTriangle(start, start + 2, start + 3); } } private static void AddVertex(VertexHelper vh, Vector2 position, Color tint) { UIVertex vertex = UIVertex.simpleVert; vertex.position = position; vertex.color = tint; vh.AddVert(vertex); } // Le contour du trou, en pixels, centré sur son propre cadre et parcouru dans // le sens anti-horaire. private List Outline() { List points = new(); Vector2 half = rectTransform.rect.size * 0.5f; if (half.x <= 0f || half.y <= 0f) return points; if (kind == Kind.Round) { int steps = ArcSteps * 4; for (int i = 0; i < steps; i++) { float angle = i * 2f * Mathf.PI / steps; points.Add(new Vector2(Mathf.Cos(angle) * half.x, Mathf.Sin(angle) * half.y)); } return points; } // Un rayon plus grand que la demi-largeur déborderait sur le coin voisin. float radius = kind == Kind.RoundedRectangle ? Mathf.Clamp(_cornerPixels, 0f, Mathf.Min(half.x, half.y)) : 0f; Vector2[] centers = { new Vector2(half.x - radius, half.y - radius), // haut-droite new Vector2(-half.x + radius, half.y - radius), // haut-gauche new Vector2(-half.x + radius, -half.y + radius), // bas-gauche new Vector2(half.x - radius, -half.y + radius), // bas-droite }; for (int corner = 0; corner < 4; corner++) { // Sans arrondi, le centre de l'arc est le coin lui-même. if (radius <= 0f) { points.Add(centers[corner]); continue; } for (int i = 0; i <= ArcSteps; i++) { float angle = (corner * 90f + 90f * i / ArcSteps) * Mathf.Deg2Rad; points.Add(centers[corner] + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius); } } return points; } }