using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; using Unity.VisualScripting; using System; using System.Linq; using AutoTiling; using UnityEngine.UI; using UnityEngine.Rendering.Universal; public class AI : MonoBehaviour { private static WaitForSecondsRealtime _waitForSecondsRealtime1 = new WaitForSecondsRealtime(1); private static WaitForSecondsRealtime _waitForSecondsRealtime2 = new WaitForSecondsRealtime(2); public static List> BASEPBLB = new();// public static List WallSelected = new();//w1,w2... [SerializeField] GameObject PNL; [SerializeField] GameObject SELECTOR; static GameObject NextObject; static GameObject PreviousObject; static int _aiBusy = 0; // nb de sous-coroutines AI en cours (sink/tall) -> AiProcess attend qu'il retombe à 0 // TEST : à true, AiProcess s'arrête après la construction (pas de PDF, pas d'email, pas de restart) // pour inspecter le placement. Remettre à false pour réactiver la chaîne complète. public static bool TestBuildOnly = true; // ── Sections de mur ──────────────────────────────────────────────────────── // Numérotation UNIFIÉE bas/haut, du début du mur (1) vers la fin, sens de l'horloge : // - un pan de mur plein entre deux ouvertures = une section (bas ET haut). // - une FENÊTRE ajoute une section « sous-fenêtre » (bas + évier seulement ; les caissons // hauts la sautent automatiquement). // - une porte / opening / patio = un trou, PAS une section (rien devant). // Ex. mur à 1 fenêtre -> 3 sections : 1=avant, 2=sous-fenêtre, 3=après. // _G.WallSelected = "m3" -> toutes les sections disponibles // _G.WallSelected = "m3_1,3" -> sections 1 et 3 (pas de bas sous la fenêtre) class WallSection { public float Start; public float End; public bool UnderWindow; } static Dictionary> WallSections = new(); // mur "m3" -> sections autorisées (absent = toutes) static Dictionary> _sections = new(); // mur "m3" -> intervalles (distance depuis le début) // Remplit WallSelected (noms normalisés "m3") et WallSections à partir des entrées brutes. // Accepte "m3", "w3" ou "3" (avec ou sans préfixe), et le suffixe de sections "_1,2". static void SetWallSelectedFrom(List raw) { WallSelected = new(); WallSections = new(); if (raw == null) return; foreach (string entry in raw) { if (string.IsNullOrEmpty(entry)) continue; int u = entry.IndexOf('_'); string numberPart = (u >= 0 ? entry[..u] : entry); // Normalise en "m" : le reste du code AI (Get.o1, WallStartPoint, [1..]) l'exige. string digits = new string(numberPart.Where(char.IsDigit).ToArray()); if (digits.Length == 0) continue; // entrée sans numéro -> ignorée string name = "m" + digits; if (!WallSelected.Contains(name)) WallSelected.Add(name); if (u >= 0) { var set = new HashSet(); foreach (string s in entry[(u + 1)..].Split(',')) if (int.TryParse(s.Trim(), out int n)) set.Add(n); if (set.Count > 0) WallSections[name] = set; } } } static int WallIndex(string wall) { string digits = new string(wall.Where(char.IsDigit).ToArray()); return int.TryParse(digits, out int n) ? n : 0; } // Ordre de remplissage : le mur « de droite » (suivant dans le sens horaire = index supérieur) est // rempli EN PREMIER, pour que le mur courant puisse ensuite détecter le caisson au début du mur // suivant et se dégager au coin. Sans ça, un mur rempli avant son voisin déborderait sur lui. static List WallsFillOrder() { var list = new List(WallSelected); list.Sort((a, b) => WallIndex(b).CompareTo(WallIndex(a))); // index décroissant return list; } // Fige les sections : à appeler après DeleteAllCab (les caissons sont retirés ; il reste les // ouvertures ET les appareils non supprimés — four "stov" et frigo "frid" survivent à DeleteAllCab). static void ComputeWallSections() { _sections = new(); foreach (string wall in WallSelected) { string w = wall.Replace("w", "m"); Vector3 start = AiGet.WallStartPoint(w); Vector3 dir = (AiGet.WallEndPoint(w) - start).normalized; float wallLen = Vector3.Distance(start, AiGet.WallEndPoint(w)); // Objets du mur qui découpent en sections, avec leur étendue le long du mur : // - FENÊTRE -> ajoute une section « sous-fenêtre » (bas seulement) // - porte / opening / patio / appareil / four / frigo -> un trou (divise, sans section) var feats = new List<(float s, float e, bool window)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[22].Replace("w", "m") != w) continue; string name4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; bool isWindow = o[2] == "Window"; bool isGap = o[2] == "Door" || o[2] == "Opening" || o[2] == "Patio" || o[2] == "Appliance" || name4 == "stov" || name4 == "frid"; if (!isWindow && !isGap) continue; GameObject g = Get.o2("SCENE", o[0]); Transform L = g != null ? g.transform.Find("PBLB") : null; Transform R = g != null ? g.transform.Find("PBRB") : null; if (L == null || R == null) continue; float dL = Vector3.Dot(L.position - start, dir); float dR = Vector3.Dot(R.position - start, dir); feats.Add((Mathf.Min(dL, dR), Mathf.Max(dL, dR), isWindow)); } feats.Sort((x, y) => x.s.CompareTo(y.s)); var secs = new List(); float cursor = 0f; foreach (var f in feats) { if (f.s > cursor + 0.5f) secs.Add(new WallSection { Start = cursor, End = f.s, UnderWindow = false }); if (f.window) secs.Add(new WallSection { Start = f.s, End = f.e, UnderWindow = true }); cursor = Mathf.Max(cursor, f.e); } if (wallLen > cursor + 0.5f) secs.Add(new WallSection { Start = cursor, End = wallLen, UnderWindow = false }); _sections[w] = secs; } } // Section (1-based) contenant la distance d ; 0 si dans un trou (porte). static int SectionIndexAtDist(string wall, float d) { if (!_sections.TryGetValue(wall, out var secs)) return 1; for (int i = 0; i < secs.Count; i++) if (d >= secs[i].Start - 0.5f && d <= secs[i].End + 0.5f) return i + 1; return 0; } // La section contenant ce point est-elle autorisée ? isWallCab -> les caissons hauts sautent le sous-fenêtre. static bool SectionAllowed(string wall, Vector3 point, bool isWallCab = false) { wall = wall.Replace("w", "m"); if (!WallSections.TryGetValue(wall, out var allowed)) return true; // mur sans restriction if (!_sections.TryGetValue(wall, out var secs)) return true; Vector3 start = AiGet.WallStartPoint(wall); Vector3 dir = (AiGet.WallEndPoint(wall) - start).normalized; float d = Vector3.Dot(point - start, dir); int idx = SectionIndexAtDist(wall, d); if (idx == 0) return false; if (isWallCab && secs[idx - 1].UnderWindow) return false; return allowed.Contains(idx); } // Le delta [a,b] tombe-t-il dans une section « sous-fenêtre » ? (milieu du delta -> section). static bool IsUnderWindowDelta(string wall, Vector3 a, Vector3 b) { wall = wall.Replace("w", "m"); if (!_sections.TryGetValue(wall, out var secs)) return false; Vector3 start = AiGet.WallStartPoint(wall); Vector3 dir = (AiGet.WallEndPoint(wall) - start).normalized; float mid = Vector3.Dot(((a + b) * 0.5f) - start, dir); int idx = SectionIndexAtDist(wall, mid); return idx > 0 && secs[idx - 1].UnderWindow; } // Espace solide (sans fenêtre/porte) au coin de départ (fromEnd=false) ou de fin (true), lu dans // le modèle de sections. Fiable pour les fenêtres, contrairement à la détection par hauteur. static float CornerSolidSpace(string wall, bool fromEnd) { wall = wall.Replace("w", "m"); if (!_sections.TryGetValue(wall, out var secs) || secs.Count == 0) return Vector3.Distance(AiGet.WallStartPoint(wall), AiGet.WallEndPoint(wall)); WallSection s = fromEnd ? secs[secs.Count - 1] : secs[0]; if (s.UnderWindow) return 0f; // fenêtre pile au coin return s.End - s.Start; } // Découpe le delta [a,b] en sous-deltas contigus dont la section est autorisée. // Mur sans restriction -> renvoie [a,b] tel quel (comportement inchangé). // C'est ce qui permet de COUPER un run de base à la fenêtre quand le sous-fenêtre est désactivé. // Un delta bordé par une COLONNE doit faire au moins 24" pour être rempli : une tranche plus // étroite coincée contre un garde-manger n'a pas d'allure et ne porte rien d'utile. // AVANT : le delta de FIN était sauté DÈS QUE le dernier obstacle était une colonne, quelle que // soit sa longueur — un mur qui offrait encore 40" après la colonne restait nu. const float MinDeltaNextToTall = 24f; static bool TallDeltaTooShort(GameObject obstacle, List delta) { if (obstacle == null || GetObjectADN(obstacle, 4) != "T1") return false; return Vector3.Distance(delta[0], delta[1]) < MinDeltaNextToTall; } // Variante pour le delta de FIN des caissons hauts, dont le point de départ n'est calculé qu'après // le test : on le reconstruit ici plutôt que de dupliquer le calcul. static bool TallDeltaTooShortAtEnd(GameObject obstacle, Vector3 direction, Vector3 endPoint) { if (obstacle == null || GetObjectADN(obstacle, 4) != "T1") return false; Vector3 s = obstacle.transform.Find("PBRB").position + AiGet.Gap(obstacle.name) * direction; return Vector3.Distance(s, endPoint) < MinDeltaNextToTall; } // Jeu déclaré pour une fenêtre de ce mur (AiGet.Gap), 0 si le mur n'en porte aucune. Passe par la // table des jeux plutôt qu'une constante locale, pour rester aligné sur les caissons hauts. static float WindowGapOnWall(string wall) { string wnum = wall.Replace("m", "w"); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum || o[2] != "Window") continue; return AiGet.Gap(o[0]); } return 0f; } static List> AllowedSubDeltas(string wall, Vector3 a, Vector3 b, bool isWallCab) { wall = wall.Replace("w", "m"); var result = new List>(); if (!WallSections.TryGetValue(wall, out var allowed) || !_sections.TryGetValue(wall, out var secs)) { result.Add(new() { a, b }); return result; } Vector3 start = AiGet.WallStartPoint(wall); Vector3 dir = (AiGet.WallEndPoint(wall) - start).normalized; float da = Vector3.Dot(a - start, dir); float db = Vector3.Dot(b - start, dir); float lo = Mathf.Min(da, db), hi = Mathf.Max(da, db); bool reversed = da > db; // Jeu à garder de part et d'autre d'une fenêtre que le run NE TRAVERSE PAS. Une fenêtre n'est // pas dans la liste d'obstacles des caissons BAS — AllObjectsOnWall les cherche dans la bande // FloorLine..FloorLine+10, sous l'appui — donc AddCabinetBaseToScene ne peut pas appliquer // AiGet.Gap dessus. C'est ICI, là où le run est coupé par la section, que ça doit se faire ; // sans ça le caisson bas venait coller la fenêtre alors que le haut gardait son jeu. float winGap = WindowGapOnWall(wall); // Regroupe en stretches contigus les portions autorisées incluses dans [lo,hi]. var stretches = new List<(float s, float e)>(); float curS = 0, curE = 0; bool inRun = false; bool prevBlockedWindow = false; for (int i = 0; i < secs.Count; i++) { float s = Mathf.Max(lo, secs[i].Start); float e = Mathf.Min(hi, secs[i].End); bool overlaps = e > s + 0.5f; bool allowedSec = allowed.Contains(i + 1) && !(isWallCab && secs[i].UnderWindow); if (overlaps && allowedSec) { if (inRun && s <= curE + 0.5f) curE = e; // section adjacente -> on prolonge else { if (inRun) stretches.Add((curS, curE)); curS = prevBlockedWindow ? s + winGap : s; // départ décollé de la fenêtre curE = e; inRun = true; } prevBlockedWindow = false; } else { if (inRun) { if (overlaps && secs[i].UnderWindow) curE -= winGap; // fin décollée de la fenêtre stretches.Add((curS, curE)); inRun = false; } prevBlockedWindow = overlaps && secs[i].UnderWindow; } } if (inRun) stretches.Add((curS, curE)); if (reversed) stretches.Reverse(); // conserver le sens de pose a -> b foreach (var (s, e) in stretches) { Vector3 pa = start + dir * s; Vector3 pb = start + dir * e; result.Add(reversed ? new() { pb, pa } : new() { pa, pb }); } return result; } public void StartPnl() { _G.MesureONOFF = true; DOIT.CloseALLPNL(); PNL.SetActive(true); Set.alpha(Get.o2("Panel_SCENE/PNL", "Ai-Controls"), true); _G.PON = "AIpnl"; //WallSelected ??= _G.WallSelected; //ShowWallSelected(); SetWallSelectedFrom(_G.WallSelected); // sépare noms de murs et sections ("m3_1,2") //WallSelected ??= SceneModeManager.Instance.SetSceneMode(SceneModes.Panel); GameObject[] tohide = GameObject.FindGameObjectsWithTag("tohide"); foreach (GameObject see in tohide) { Set.alpha(see, false); } SELECTOR.SetActive(false); _AI.On = true; _AI.FloorLine = -_G.HEIGHT * 0.5f; _AI.CeilingLine = _G.HEIGHT * 0.5f; _AI.CabinetHeight = 35; SetAILibrairy(); float BaseHeight = UIT.ValueFloat(Library.Cabinet,_AI.BASE,Header.B); float WallHeight = UIT.ValueFloat(Library.Cabinet,_AI.WALL,Header.B); _AI.BaseHeightPosition = -_G.HEIGHT * 0.5f + BaseHeight * 0.5f + 1.25f * 0.5f; _AI.WallHeightPosition = -_G.HEIGHT * 0.5f + 54 + WallHeight * 0.5f; AiButton.Instance.GetTitle().text = AiText.Trans("Hi"); AiButton.Instance.GetButtonGoNext().SetActive(true); AiButton.Instance.GetModification().SetActive(false); AiButton.Instance.GetSelector().SetActive(false); AiButton.Instance.GetPlus().SetActive(false); AiButton.Instance.GetGoBackButton().SetActive(false); AiButton.Instance.GetDeleteAllCabButton().SetActive(AiStep.CabinetOnScene()); if (WallSelected == null || WallSelected.Count == 0) ShowOnlySelectWall(); else ShowAllSteps(); } public static void SetAILibrairy() { if (_G.PATH == "kitchensdepot") { _AI.Door1Thickness=0.75f; _AI.CB90 = "LS36";//Corner Bse 90 _AI.CB9036 = "";//Pas de coin plus large chez ce client _AI.CBDEADL = "DLB48-51";//Corner _AI.BASE Dead Left _AI.CBDEADR = "DLB48-51";//Corner _AI.BASE Dead Right _AI.CW90 = "WPC2430";//Corner Wall 90 _AI.CWDEADL = "";//Corner Wall Dead Left _AI.CWDEADR = "";//Corner Wall Dead Right _AI.BASE = "B24"; _AI.WALL = "W2430"; _AI.TALL = "TP302484"; _AI.TALLSMALL = ""; _AI.DRAWER = "DB24"; _AI.BASESINK = "SB36"; _AI.FRIDGE = "W331524"; _AI.MICROWAVE = "BMC30"; _AI.PANEL_ISLAND = "PLY-1/2"; _AI.PANEL_FRIDGE = "REP1-1/2"; // _AI.BaseDepth = 23.375f; _AI.WallDepth = 11.375f; _AI.Door1 = "6?16"; _AI.Door2 = "5?15"; _AI.Kick1 = "1?16?90"; _AI.Kick2 = "1?15?90"; //Base Cabinet Wider _AI.Base9 = "B09"; _AI.Base12 = "B12"; _AI.Base15 = "B15"; _AI.Base18 = "B18"; _AI.Base21 = "B21"; _AI.Base24 = "B24"; _AI.Base27 = "B27"; _AI.Base30 = "B30"; _AI.Base33 = "B33"; _AI.Base36 = "B36"; _AI.TallUnder = "DB30"; //Drawer _AI.Drawer12 = "DB12"; _AI.Drawer15 = "DB15"; _AI.Drawer18 = "DB18"; _AI.Drawer21 = "DB18"; _AI.Drawer24 = "DB24"; _AI.Drawer27 = ""; _AI.Drawer30 = "DB30"; _AI.Drawer33 = ""; _AI.Drawer36 = "DB36"; //Wall Cabinet Wider _AI.Wall9 = "W0930"; _AI.Wall12 = "W1230"; _AI.Wall15 = "W1530"; _AI.Wall18 = "W1830"; _AI.Wall21 = "W2130"; _AI.Wall24 = "W2124"; _AI.Wall27 = "W2730"; _AI.Wall30 = "W3030"; _AI.Wall33 = "W3330"; _AI.Wall36 = "W3630"; _AI.Wall24OS = "DW2412"; _AI.Wall30OS = ""; // pas de modèle over sink 30" chez ce client } if (_G.PATH == "baindepot") { _AI.Door1Thickness=0.75f; //Base Corner _AI.CB90 = "KUI-BLSH3333-11";//Corner Bse 90 _AI.CBDEADL = "KUI-BC45-11";//Corner _AI.BASE Dead Left _AI.CBDEADR = "KUI-BC45-11";;//Corner _AI.BASE Dead Right _AI.CB9036 = "KUI-BLSH3636-11";//Corner Bse 90 36 _AI.CW90 = "KUI-WDC2430-11";//Corner Wall 90 _AI.CWDEADL = "";//Corner Wall Dead Left _AI.CWDEADR = "";//Corner Wall Dead Right _AI.CW90GL = "KUI-WDC2430-11GL";//Corner Wall 90 Glass _AI.BASE = "KUI-BH24-2-11"; _AI.WALL = "KUI-W2430-2-11"; _AI.TALL = "KUI-HD3DB30-11"; _AI.TALLSMALL = "KUI-TP188424-11"; _AI.DRAWER = "KUI-3DB24-11"; _AI.BASESINK = "KUI-BHS3036-2-11"; _AI.FRIDGE = "KUI-W331224-11"; _AI.MICROWAVE = "KUI-WMW2430-11"; _AI.PANEL_ISLAND = "KUI-DWPANMWH-11"; _AI.PANEL_FRIDGE = "KUI-PAN2484MVH-11"; // _AI.BaseDepth = 23.875f; _AI.WallDepth = 12.875f; _AI.Door1 = "6?16"; _AI.Door2 = "5?15"; _AI.Kick1 = "1?16?90"; _AI.Kick2 = "1?15?90"; //Base Cabinet Wider _AI.Base9 = "KUI-BH09-11"; _AI.Base12 = "KUI-BH12-11"; _AI.Base15 = "KUI-BH15-11"; _AI.Base18 = "KUI-BH18-11"; _AI.Base21 = ""; _AI.Base24 = "KUI-BH24-2-11"; _AI.Base27 = ""; _AI.Base30 = "KUI-BH30-2-11"; _AI.Base33 = ""; _AI.Base36 = "KUI-BH36-2-11"; _AI.TallUnder = "DB30"; //Drawer _AI.Drawer12 = ""; _AI.Drawer15 = "KUI-3DB15-11"; _AI.Drawer18 = "KUI-4DB18-11"; _AI.Drawer21 = ""; _AI.Drawer24 = "KUI-3DB24-11"; _AI.Drawer27 = ""; _AI.Drawer30 = "KUI-3DB30-11"; _AI.Drawer33 = ""; _AI.Drawer36 = ""; //Wall Cabinet Wider _AI.Wall9 = "KUI-W0930-11"; _AI.Wall12 = "KUI-W1230-11"; _AI.Wall15 = "KUI-W1530-11"; _AI.Wall18 = "KUI-W1830-11"; _AI.Wall21 = ""; _AI.Wall24 = "KUI-W2430-2-11"; _AI.Wall27 = ""; _AI.Wall30 = "KUI-W3030-2-11"; _AI.Wall33 = "KUI-W3330"; _AI.Wall36 = ""; _AI.Wall24OS = ""; _AI.Wall30OS = "KUI-W3015-11"; //Wall cabinet with Glass _AI.Wall12GL = "KUI-W1230-11GL"; _AI.Wall15GL = "KUI-W1530-11GL"; _AI.Wall24GL = "KUI-W2430-2-11CL"; _AI.WallGL = "KUI-W2430-2-11CL"; _AI.Wall30OSGL= "KUI-W3015-11GL"; //NOLDING _AI.FASCIA = "KUI-F384-11"; _AI.OGEE = ""; _AI.VALANCE = "KUI-L-V96-11"; //_G.Island="Return-Stool"; } if (_G.PATH == "collection_elite") { _AI.Door1Thickness=0.75f; _AI.CB90 = "RD-BC36";//Corner Bse 90 _AI.CB9036 = "";//Pas de coin plus large chez ce client _AI.CBDEADL = "RD-BCM36";//Corner _AI.BASE Dead Left _AI.CBDEADR = "RD-BCM36";//Corner _AI.BASE Dead Right _AI.CW90 = "RD-HC2424";//Corner Wall 90 _AI.CWDEADL = "";//Corner Wall Dead Left _AI.CWDEADR = "";//Corner Wall Dead Right _AI.BASE = "RD-B24"; _AI.WALL = "RD-HG24"; _AI.TALL = "RD-GM2449"; _AI.TALLSMALL = ""; _AI.DRAWER = "RD-B2T24"; _AI.BASESINK = "RD-B36"; _AI.FRIDGE = "REP1-1/2";// REP1-1/2 RD-HTPP3312 _AI.Door1Thickness=0.75f; _AI.MICROWAVE = "RD-BMO24"; _AI.PANEL_ISLAND = "RD-PANDW"; _AI.PANEL_FRIDGE = "RD-3085"; // _AI.BaseDepth = 23.625f; _AI.WallDepth = 11.625f; _AI.Door1 = "1?1"; _AI.Door2 = "2?2"; _AI.Kick1 = "1?1?90"; _AI.Kick2 = "2?2?90"; //Base Cabinet Wider _AI.Base9 = ""; _AI.Base12 = "RD-B12"; _AI.Base15 = "RD-B15"; _AI.Base18 = "RD-B18"; _AI.Base21 = ""; _AI.Base24 = "RD-B24"; _AI.Base27 = ""; _AI.Base30 = "RD-B30"; _AI.Base33 = "RD-B33"; _AI.Base36 = "RD-B36"; _AI.TallUnder = "RD-B2T24"; //Drawer _AI.Drawer12 = "RD-B3T12"; _AI.Drawer15 = "RD-B3T15"; _AI.Drawer18 = ""; _AI.Drawer21 = ""; _AI.Drawer24 = "RD-B2T24"; _AI.Drawer27 = ""; _AI.Drawer30 = "RD-B2T30"; _AI.Drawer33 = ""; _AI.Drawer36 = ""; //Wall Cabinet Wider _AI.Wall9 = ""; _AI.Wall12 = "RD-HG12"; _AI.Wall15 = "RD-HG15"; _AI.Wall18 = "RD-HG18"; _AI.Wall21 = ""; _AI.Wall24 = "RD-HG24"; _AI.Wall27 = ""; _AI.Wall30 = "RD-HG30"; _AI.Wall33 = "RD-HG33"; _AI.Wall36 = "RD-HG36"; _AI.Wall24OS = ""; _AI.Wall30OS = ""; // pas de modèle over sink chez ce client } _AI.HOOD = "HOOW101"; _AI.DISHWASHER = GetApliancesModel("DISH101"); } //-----------------------------------ROOM CHECK STOVE FRIDGE-------------------------------------------------- public void CheckIfStove() { bool hasStove = false; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][4] == "stov") { hasStove = true; break; } } Transform root = PNL.transform.parent != null ? PNL.transform.parent : PNL.transform; Transform btn = root.Find("Ai-Controls/STEPS/Add Stove"); if (btn != null) btn.gameObject.SetActive(!hasStove); //if (!hasStove) AddStove(); DOIT.AllSelOff(); } public void CheckIfFridge() { bool hasFridge = false; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][4] == "frid") { hasFridge = true; break; } } Transform root = PNL.transform.parent != null ? PNL.transform.parent : PNL.transform; Transform btn = root.Find("Ai-Controls/STEPS/Add Fridge"); if (btn != null) btn.gameObject.SetActive(!hasFridge); //if (!hasFridge) AddFrige(); DOIT.AllSelOff(); } Transform GetStepsRoot() { Transform root = PNL.transform.parent != null ? PNL.transform.parent : PNL.transform; return root.Find("Ai-Controls/STEPS"); } void ShowOnlySelectWall() { Transform steps = GetStepsRoot(); if (steps == null) return; foreach (Transform child in steps) child.gameObject.SetActive(child.name == "Select wall Cabinet"); } void ShowAllSteps() { Transform steps = GetStepsRoot(); if (steps == null) return; Debug.Log("ShowAllSteps: childCount=" + steps.childCount); if (steps.childCount == 0 && steps.GetComponentsInChildren(true).Length > 1) { string nested = ""; foreach (Transform t in steps.GetComponentsInChildren(true)) if (t != steps) nested += t.name + "(depth) | "; Debug.Log("ShowAllSteps: nested children: " + nested); } foreach (Transform child in steps) { bool show = child.name != "Select wall Cabinet"; child.gameObject.SetActive(show); } } public void SelectWallCabinet() { StartCoroutine(CoroutineSelectWall()); } IEnumerator CoroutineSelectWall() { WallSelected = new(); _M.PH(33, 0, "ffffff", 1, 1); SceneModeManager.Instance.SetSceneMode(SceneModes.Ai); yield return new WaitUntil(() => WallSelected != null && WallSelected.Count > 0); ShowAllSteps(); } //------------------------------------Play-------------------------------------------------- public void ProcessAi() { if (WallSelected != null && WallSelected.Count > 0) { StartCoroutine(AiProcess()); } else { WallSelected = new(); for (int i = 0; i < _G.OBJnum; i++) { if (DOIT.exist(i)) { string[] ADN = _G.OBJs[i]; if (ADN[2] == "Cabinet" && ADN[22] != "floor") { string WallNumber = ADN[22].Replace("w", "m"); if (!WallSelected.Contains(WallNumber)){WallSelected.Add(ADN[22].Replace("w", "m"));} } } } StartCoroutine(AiProcess()); } } IEnumerator AiProcess() { _G.Island="Peninsula-Stool"; //UIPathEffect.StartMovement(); Get.o2("HIDER", "MESSAGE").SetActive(false); DeleteAllCab(); // WallSelected = ReorderWallString(WallSelected); yield return _waitForSecondsRealtime1; SetGlobals(); // Four et frigo AVANT ComputeWallSections : les sections les figent comme trous, et la hotte // (CaroutineAddHood) / le caisson frigo (AddFridgeCabinetCo) ont besoin qu'ils soient en place. // Les deux coroutines sortent d'elles-mêmes si l'appareil existe déjà (même test que // CheckIfStove/CheckIfFridge). Placement calculé après DeleteAllCab : les trous sont mesurés // sur les ouvertures seules, pas sur les caissons de la passe précédente. if (WallSelected != null && WallSelected.Count > 0) { yield return StartCoroutine(CoroutineAddStove(false)); yield return StartCoroutine(CoroutineAddFrige(false)); } // Fige les sections MAINTENANT : DeleteAllCab a retiré les caissons, seules les ouvertures restent. ComputeWallSections(); _aiBusy = 0; // sécurité : repartir de zéro _tallSpots.Clear(); // emplacements de colonne de la passe précédente _peninsulaRowCabs.Clear(); // rangee avant de péninsule de la passe précédente _peninsulaCornerCabs.Clear(); // coins de péninsule de la passe précédente _peninsulaReturnCabs.Clear(); // retour du L de la passe précédente _peninsulaReserved.Clear(); // réserves de projection de la passe précédente // Chaque commande est ATTENDUE jusqu'à sa vraie fin (au lieu d'un délai fixe qui laissait // la commande suivante démarrer trop tôt). // sink/tall lancent des sous-coroutines comptées -> on attend que _aiBusy retombe à 0. AddSinkCabinet(); yield return null; yield return new WaitWhile(() => _aiBusy > 0); yield return StartCoroutine(AddCornerBASECabinetCo()); AddTALLCabinet(); yield return null; yield return new WaitWhile(() => _aiBusy > 0); // Recale four et frigo d'au plus ±3" pour que les travées qu'ils délimitent tombent sur des // largeurs de caissons existantes. ICI et pas juste après leur pose : les bornes des travées // ne sont connues qu'une fois l'évier, les coins et les colonnes en place. Ce qui est déjà // collé à l'appareil est déplacé avec lui. yield return StartCoroutine(CoroutineOptimizeAppliancePositions()); // Les appareils ont pu bouger -> les sections figées plus haut sont périmées. ComputeWallSections // ne lit que les ouvertures et les appareils, jamais les caissons : la relancer maintenant est sûr. ComputeWallSections(); // Péninsule APRÈS le four, le frigo, le caisson évier et les colonnes (garde-manger) : sa // place se mesure sur ce qui est DEJA pose, pas sur un sol encore vide — c'est la difference // avec l'ancienne version, qui la posait avant les colonnes sur un simple bout d'allee libre. // Toujours AVANT la rangee basse : son caisson de coin devient un obstacle et la rangee se // compose autour, dans sa continuite. yield return StartCoroutine(CoroutineAddPeninsula()); yield return new WaitWhile(() => _aiBusy > 0); yield return StartCoroutine(CoroutineAdd_BASECabinet()); yield return StartCoroutine(AddCornerWallCabinetCo()); // Caisson COURT au-dessus de l'évier, posé APRÈS le coin mural mais AVANT le remplissage : // il devient un obstacle et les rangées se composent de part et d'autre. Posé après coup, il // fallait au contraire remplacer le caisson qui tombait là, d'une largeur quelconque // (9, 12, 15, 21...) pour laquelle le catalogue n'a pas de modèle "over sink". yield return StartCoroutine(CoroutineAddCabinetOverSink()); yield return StartCoroutine(CaroutineAddHood()); yield return StartCoroutine(CoroutineAddWallCabinet()); // Filet de sécurité : si un caisson pleine hauteur s'est quand même retrouvé au-dessus de // l'évier, on le raccourcit ; sinon la passe ne fait que poser le dosseret du caisson évier. yield return StartCoroutine(CoroutineVerifyWallCabinetOverSink()); yield return StartCoroutine(AddFridgeCabinetCo()); yield return StartCoroutine(AddIslandCabinetonFloor()); // Filet : avec "Island = Sink", AddSinkCabinet s'est abstenu de poser un évier au mur. Si // AddIslandCabinetonFloor n'a finalement pas pu placer d'îlot, on rattrape en CONVERTISSANT un // caisson existant — surtout pas en en posant un nouveau : à ce stade les murs sont pleins. if (!SinkExists()) { print("[AI] Aucun evier apres l'ilot -> conversion d'un caisson existant."); PlaceSinkOnExistingCabinet(); yield return _waitForSecondsRealtime2; } yield return StartCoroutine(CoroutineAddDishwasher()); yield return StartCoroutine(CoroutineAddMicrowave()); // Recolle les groupes de caissons contre l'obstacle prioritaire de leur section (panneau, // évier, coin, lave-vaisselle) AVANT le dosseret, les tiroirs et les fillers/recover : ces // trois passes lisent les positions finales. yield return StartCoroutine(CoroutineAdjustSections()); AddDrawers(); // synchrone yield return null; AddBackSplash(); // Le sens d'ouverture est désormais fixé à la CONSTRUCTION, dans Set_TemporySC. La passe // CoroutineFlipHandles est donc débranchée du pipeline : elle recalculait la même chose plus // tard, avec des références différentes en rangée haute (fenêtre d'évier et hotte), et // écrasait la décision prise à la pose. Elle reste appelable depuis son bouton. //yield return StartCoroutine(CoroutineFlipHandles()); yield return StartCoroutine(CoroutineAddFillers()); yield return StartCoroutine(CoroutineAddStools()); yield return StartCoroutine(CoroutineAddLamps()); // AddFloor(); PaintWalls(); yield return StartCoroutine(CoroutineAddSpotCeilling()); yield return StartCoroutine(CoroutineAddGlassDoor()); yield return StartCoroutine(CoroutineAddMoldings()); yield return _waitForSecondsRealtime2; // DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; //ButtonDoneToggle.NotifyDone(); AllMeshDeselect(); DASH.HDsetBTNcolor(true); AiStep.ShowModification(); UIPathEffect.StopMovement(); _G.CameraCliping = 2; Camera.main.GetComponent().Hide(); // Génération automatique du PDF une fois tout l'îlot/cuisine assemblé. // TEST : TestBuildOnly coupe PDF + email + restart (déclenchés par ClickPDF_AI -> Calculate). if (TestBuildOnly) { Debug.Log("[AI] TestBuildOnly = true -> construction seule (pas de PDF/email/restart)."); yield break; } yield return _waitForSecondsRealtime1; // Avant sauvegarde : calculer l'offset mur-relatif (ADN[14]) de chaque objet. Les caissons AI // ont C[14] = position absolue (mis par CreateCAB) ; sans ce recalcul, la reposition au // chargement (DASH.RepositionObjectOnWallCoroutine) voit ADN[14]==position et bascule le // caisson sur "floor" (perte du mur, de la profondeur et de la lumière sous les caissons hauts). for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; GameObject go = Get.o2("SCENE", _G.OBJs[i][0]); if (go != null) DOIT.SaveObjectPositionInData(go); } // Sauvegarde web du design AI sous le nom {email}_savefromai (voir Save_WEB.cs). string aiSaveName = (string.IsNullOrEmpty(_G.EMAIL) ? "hb3d63@gmail.com" : _G.EMAIL) + "_savefromai"; yield return StartCoroutine(Save_WEB.SaveWebAs(aiSaveName)); PDF pdf = FindAnyObjectByType(); if (pdf != null) pdf.ClickPDF_AI(); } //-------------------------------SINK CABINET------------------------------------------------------------------ // _G.Island est de la forme "Type-Fonction" : "Island-Sink", "Peninsula", "Return-Sink"... // Type : Island (îlot libre), Peninsula (rattachée au mur), Return (péninsule en L), None. // Fonction : "Sink" quand le meuble doit porter l'évier. static string IslandType() { if (string.IsNullOrEmpty(_G.Island)) return "None"; return _G.Island.Split('-')[0].Trim(); } static string IslandFunction() { if (string.IsNullOrEmpty(_G.Island)) return ""; string[] p = _G.Island.Split('-'); return p.Length > 1 ? p[1].Trim() : ""; } static bool IslandTypeIs(string type) => IslandType().Equals(type, StringComparison.OrdinalIgnoreCase); // Péninsule : rattachée au mur par un caisson de coin, droite ("Peninsula") ou en L ("Return"). static bool WantsPeninsula() => IslandTypeIs("Peninsula") || IslandTypeIs("Return"); // Le design demande-t-il des TABOURETS ? "Island-Stool", "Peninsula-Stool", // "Island-Stool-Sink"... Sans ce suffixe — "Island" seul, "Island-Sink" — aucun tabouret, // et donc aucun débord de comptoir pour les asseoir. // On balaie TOUS les segments et pas seulement le premier : les designs combinent les // fonctions ("Island-Stool-Sink"), et ne lire que _G.Island.Split('-')[1] rate le cas où // "Stool" arrive en deuxième position. // Debord de comptoir de la banquette d'un ILOT. La peninsule a le sien // (PeninsulaStoolOverhang). const float IslandStoolOverhang = 10f; // Sans tabourets, le comptoir garde une projection ORDINAIRE de 1" : ni assise, ni arete nue // alignee sur le caisson. const float NoStoolOverhang = 1f; static float StoolOverhang() => WantsStools() ? IslandStoolOverhang : NoStoolOverhang; // Deux assises possibles sur un RETURN : // "Stool" -> le long de l'ALLEE, sur la face exposee de la rangee avant (cas d'origine) ; // "StoolR" -> AU BOUT, sur la face exposee du RETOUR du L. // Le debord de comptoir qui sert d'assise ne va QUE du cote qui recoit les tabourets ; // l'autre face garde la projection ordinaire de 1". static bool WantsStools() { if (string.IsNullOrEmpty(_G.Island)) return false; string[] parts = _G.Island.Split('-'); for (int i = 1; i < parts.Length; i++) { string f = parts[i].Trim(); if (f.Equals("Stool", StringComparison.OrdinalIgnoreCase)) return true; if (f.Equals("StoolR", StringComparison.OrdinalIgnoreCase)) return true; } return false; } // Assise AU BOUT du L, sur le retour ("StoolR"). static bool WantsStoolsAtReturn() { if (string.IsNullOrEmpty(_G.Island)) return false; string[] parts = _G.Island.Split('-'); for (int i = 1; i < parts.Length; i++) if (parts[i].Trim().Equals("StoolR", StringComparison.OrdinalIgnoreCase)) return true; return false; } // Assise le long de l'allee : des tabourets, mais pas au bout. static bool StoolsAlongAisle() => WantsStools() && !WantsStoolsAtReturn(); // Debord d'ASSISE sur la face exposee de la RANGEE AVANT (le long de l'allee). static float RowSeatProjection() => StoolsAlongAisle() ? PeninsulaStoolOverhang : NoStoolOverhang; // Debord d'ASSISE sur la face exposee du RETOUR du L (au bout). static float ReturnSeatProjection() => WantsStoolsAtReturn() ? PeninsulaStoolOverhang : NoStoolOverhang; // Vrai quand le design demande un meuble AVEC ÉVIER : l'évier y va, pas au mur. // Accepte l'ancien format où _G.Island valait directement "Sink". static bool IslandTakesSink() => IslandFunction().Equals("Sink", StringComparison.OrdinalIgnoreCase) || IslandType().Equals("Sink", StringComparison.OrdinalIgnoreCase); // Un îlot pourra-t-il vraiment être posé ? Reprend le début d'AddIslandCabinetonFloor — carte // d'occupation du sol puis plus grand rectangle libre — et vérifie qu'il reste de quoi loger un // îlot minimal. // Appelée AVANT la pose des caissons, elle est donc OPTIMISTE : le sol est encore dégagé, le // rectangle libre ne peut que rétrécir ensuite. C'est justement pourquoi le repli de fin de // pipeline est conservé — il rattrape le cas où l'îlot ne se fait finalement pas. static bool IslandWillFit() { // Une PÉNINSULE est adossée au mur : elle ne dépend pas du rectangle libre de l'îlot, mais // elle doit quand même loger sa rangee avant et ses allées. Mesure OPTIMISTE elle aussi — la // rangée basse n'est pas encore posée — mais elle écarte les pièces où la péninsule ne // passera jamais, au lieu de renoncer à l'évier mural pour rien. if (WantsPeninsula()) { bool room = FindPeninsulaSpot(out _); print("[AI] Peninsule possible ? " + room); return room; } if (!IslandTypeIs("Island")) return false; // reserveCabinetDepth : les caissons ne sont pas encore posés, on réserve leur profondeur sur // les murs qui vont en recevoir. Sans ça le rectangle libre sortait ~24" trop grand par mur et // l'îlot était jugé possible alors qu'il ne l'était pas — l'évier mural était écarté pour rien. // Les dimensions de la pièce viennent du polygone des murs, le four et le frigo sont déjà en // scène et comptés avec leur dégagement par l'étape 2 de la grille. BuildOccupancyGrid(out bool[,] occ, out int cols, out int rows, out _, out _, true); if (!LargestFreeRectangle(occ, cols, rows, out _, out _, out int bW, out int bH)) { print("[AI] Ilot possible ? False (aucun rectangle libre)"); return false; } float freeX = bW * ISLAND_GRID_STEP; float freeZ = bH * ISLAND_GRID_STEP; float len = Mathf.Max(freeX, freeZ); // longueur de l'îlot float depth = Mathf.Min(freeX, freeZ); // profondeur disponible // Longueur minimale : celle d'un îlot exploitable, et au moins le caisson évier puisque c'est // lui qu'on renonce à poser au mur. float sinkWide = UIT.ValueFloat(Library.Cabinet, _AI.BASESINK, Header.A); float minLen = Mathf.Max(ISLAND_MIN_LEN, sinkWide); bool fits = len >= minLen && depth >= _AI.BaseDepth + _AI.Door1Thickness + 0.5f; print("[AI] Ilot possible ? " + fits + " (libre " + len + " x " + depth + " ; requis " + minLen + " x " + (_AI.BaseDepth + _AI.Door1Thickness + 0.5f) + ")"); return fits; } // Un évier existe-t-il déjà quelque part (mur ou îlot) ? static bool SinkExists() { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[4] == "sink" || SameModel(o[1], _AI.BASESINK)) return true; } return false; } // Repli quand "Island = Sink" a écarté l'évier mural et qu'aucun îlot n'a finalement été posé. // À ce stade du pipeline TOUS les caissons sont en place : poser un nouveau caisson évier le // superposerait aux existants, car AddSinkCabinet raisonne sur un mur encore vide et ne teste // aucune occupation. On CONVERTIT donc un caisson de base déjà posé, via le même ChangeCabinet // que AddDrawers — il gère déjà la recette évier/robinet pour _AI.BASESINK. void PlaceSinkOnExistingCabinet() { float sinkWide = UIT.ValueFloat(Library.Cabinet, _AI.BASESINK, Header.A); if (sinkWide <= 0f) sinkWide = 36f; // Référence : la fenêtre d'évier si elle existe, sinon on prend le caisson le plus large. int wsi = GetWindowSinkIndex(); bool hasWin = wsi != 1000 && DOIT.exist(wsi); Vector2 win = hasWin ? new Vector2(DOIT.ConvertStringToNumber(_G.OBJs[wsi][15]), DOIT.ConvertStringToNumber(_G.OBJs[wsi][17])) : Vector2.zero; GameObject best = null; float bestScore = float.MaxValue, bestWide = 0f; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[2] != "Cabinet" || o[4] != "B1") continue; if (string.IsNullOrEmpty(o[22]) || o[22] == "floor" || o[22] == "ceil") continue; if (o[0] != null && o[0].Length >= 4 && o[0][..4] == "dish") continue; if (SameModel(o[1], _AI.BASESINK)) continue; // Au moins aussi large que le caisson évier : ChangeCabinet conserve le CENTRE, un modèle // plus large déborderait sur les voisins. float w = DOIT.ConvertStringToNumber(DOIT.CSs(o[6])); if (w + 0.5f < sinkWide) continue; GameObject go = Get.o2("SCENE", o[0]); if (go == null) continue; float score = hasWin ? (DOIT.ConvertStringToNumber(o[15]) - win.x) * (DOIT.ConvertStringToNumber(o[15]) - win.x) + (DOIT.ConvertStringToNumber(o[17]) - win.y) * (DOIT.ConvertStringToNumber(o[17]) - win.y) : -w; // pas de fenêtre -> le plus large if (score < bestScore) { bestScore = score; best = go; bestWide = w; } } if (best == null) { print("[AI] Repli evier : aucun caisson de base d'au moins " + sinkWide + " -> aucun evier."); return; } print("[AI] Repli evier : " + best.name + " (" + bestWide + ") converti en " + _AI.BASESINK); ChangeCabinet(best, _AI.BASESINK); } public void AddSinkCabinet() { SetAILibrairy(); // On ne renonce à l'évier mural QUE si un îlot pourra effectivement le porter. if (IslandTakesSink() && IslandWillFit()) { print("[AI] Island = Sink et place suffisante -> l'evier ira dans l'ilot, aucun evier au mur."); return; } //Check on all walls if there is a window string WallNumber = "No window found";// //WallSelected = new(){"m3","m4"}; //CheckON(gameObject); GameObject Window = null; Vector3 PositionToInstall; Vector3 Forward; int IndexSinkWindow = GetWindowSinkIndex(); if (IndexSinkWindow != 1000) { WallNumber = _G.OBJs[IndexSinkWindow][22]; Window = Get.o2("SCENE", _G.OBJs[IndexSinkWindow][0]); Forward = -Get.o1(WallNumber).transform.forward.normalized; PositionToInstall = Window.transform.position; PositionToInstall += (_AI.BaseDepth+_AI.Door1Thickness) * 0.5f * Forward; PositionToInstall.y = _AI.BaseHeightPosition; StartCoroutine(AddSinkCabinetToWall(_AI.BASESINK, WallNumber, PositionToInstall,true)); return; } else if (WallSelected != null && WallSelected.Count > 0) { // Aucune fenêtre ne convient -> la SECTION MURALE LA PLUS LARGE, tous murs sélectionnés // confondus. L'îlot ne prend l'évier qu'en dernier recours : AddIslandCabinetonFloor ne le // met dans l'îlot que si SinkOnWallExists() est faux. PlaceSinkOnWidestWallSection(); } } // Centre le caisson évier sur la plus large section libre des murs sélectionnés. // Remplace l'ancienne logique, qui ne regardait que WallSelected[0] et traitait séparément les cas // 0 / 1 / >2 obstacles — deux obstacles exactement n'étaient jamais couverts, et les autres murs // sélectionnés n'étaient jamais examinés. // Retourne false si aucune section n'est assez large : l'évier ira alors dans l'îlot. bool PlaceSinkOnWidestWallSection() { float sinkWide = UIT.ValueFloat(Library.Cabinet, _AI.BASESINK, Header.A); if (sinkWide <= 0f) sinkWide = 36f; string bestWall = null; float bestWidth = 0f; Vector3 bestCenter = Vector3.zero; foreach (string wall in WallSelected) { Vector3 wStart = AiGet.WallStartPoint(wall); Vector3 wEnd = AiGet.WallEndPoint(wall); Vector3 wDir = (wEnd - wStart).normalized; float wLen = Vector3.Distance(wStart, wEnd); // Occupé = ce qui touche la bande des caissons bas : four, frigo, porte, patio, et les // caissons déjà en place si la commande est lancée seule depuis son bouton. var occ = new List<(float s, float e)>(); foreach (GameObject g in AiGet.AllObjectsOnWall(wall, "All", "All", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine)) { Transform L = g.transform.Find("PBLB"); Transform R = g.transform.Find("PBRB"); if (L == null || R == null) continue; float a = Vector3.Dot(L.position - wStart, wDir); float b = Vector3.Dot(R.position - wStart, wDir); occ.Add((Mathf.Min(a, b), Mathf.Max(a, b))); } occ.Sort((x, y) => x.s.CompareTo(y.s)); // Sections libres = complément des intervalles occupés, bouts de mur inclus. float cursor = 0f; foreach (var (s, e) in occ) { ConsiderSinkSpan(wall, wStart, wDir, cursor, s, sinkWide, ref bestWall, ref bestWidth, ref bestCenter); cursor = Mathf.Max(cursor, e); } ConsiderSinkSpan(wall, wStart, wDir, cursor, wLen, sinkWide, ref bestWall, ref bestWidth, ref bestCenter); } if (bestWall == null) { print("[AI] Evier : aucune section murale >= " + sinkWide + " -> l'evier ira dans l'ilot"); return false; } Vector3 Forward = -Get.o1(bestWall).transform.forward.normalized; Vector3 pos = bestCenter + (_AI.BaseDepth + _AI.Door1Thickness) * 0.5f * Forward; pos.y = _AI.BaseHeightPosition; print("[AI] Evier centre sur la section la plus large : " + bestWall + " (largeur " + bestWidth + ")"); StartCoroutine(AddSinkCabinetToWall(_AI.BASESINK, bestWall, pos, false)); return true; } // Retient la section [s,e] si elle bat la meilleure trouvée, qu'elle est assez large pour le // caisson évier, et que son centre tombe dans une section AUTORISÉE du mur (restrictions de l'UI). static void ConsiderSinkSpan(string wall, Vector3 wStart, Vector3 wDir, float s, float e, float sinkWide, ref string bestWall, ref float bestWidth, ref Vector3 bestCenter) { float width = e - s; if (width < sinkWide || width <= bestWidth) return; Vector3 center = wStart + wDir * ((s + e) * 0.5f); if (!SectionAllowed(wall, center)) return; bestWall = wall; bestWidth = width; bestCenter = center; } IEnumerator AddSinkCabinetToWall(string Model, string WallNumber, Vector3 PositionToInstall, bool isUnderWindow) { // Section désactivée pour ce mur -> pas d'évier ici. if (!SectionAllowed(WallNumber, PositionToInstall)) yield break; _aiBusy++; print("_AI.BASESINK=="+_AI.BASESINK); float Angle = GetAngleByWall(WallNumber); Set_TemporySC(_AI.BASESINK, WallNumber, PositionToInstall, Angle, "F"); string ColorHex=ColorHEX(_G.AI_PREFERENCES["SinkFaucetColor"]); _G.SC[67] = "SINK102,32,18,"+ColorHex+"?Glossy?none"; _G.SC[68] = "FAUC101,10,10,"+ColorHex+"?Glossy?none"; _G.SC[55] = "0,0,0,0"; if(!isUnderWindow){_G.SC[55] = "0,0,0,0";} _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; CreateCAB.AddCab(); yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); ReflectionProbesUpdater.Instance.UpdateProbes(); _aiBusy--; yield return null; } /// //-------------------------------CORNER BASE CABINET------------------------------------------------------------------ public void AddCornerBASECabinet() => StartCoroutine(AddCornerBASECabinetCo()); private IEnumerator AddCornerBASECabinetCo() { if (WallSelected.Count > 1) { //GameObject Wall; string PreviousWallName; Vector3 StartPoint; Vector3 EndPoint; List Delta; foreach (string Wallstring in WallSelected) { //Wall = Get.o2("ROOM", Wallstring.Replace("m", "w")); PreviousWallName = AiGet.PreviousWallName(Wallstring); List WallObstacles = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine); List PreviousObstacles =AiGet. AllObjectsOnWall(PreviousWallName, "All", "All", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine); float AngleBetween = AiGet.AngleBetweenWallAndPrevious(Wallstring, PreviousWallName); float SpaceOnWall = AiGet.WallSpaceDeltaOnWallFrom(Wallstring,WallObstacles,"From Start"); float SpaceOnPrevious = AiGet.WallSpaceDeltaOnWallFrom(PreviousWallName,PreviousObstacles,"From End"); print("SpaceOnWall==" + SpaceOnWall+" SpaceOnPreviou===" + SpaceOnPrevious); // Caisson de coin retenu : le plus grand qui tient sur les DEUX murs sans gaspiller // plus d'espace qu'un plus petit. "" = aucun coin possible ici. string cornerModel = ChooseCornerBaseModel(SpaceOnWall, SpaceOnPrevious); bool previousSelected = WallSelected.Contains(PreviousWallName.Replace("w", "m")); bool squareCorner = AngleBetween < 91 && AngleBetween > 89; print("[AI] coin " + Wallstring + "/" + PreviousWallName + " : angle=" + AngleBetween + " (carré=" + squareCorner + ")" + " murPrécédentSélectionné=" + previousSelected + " espaces=" + SpaceOnWall + "/" + SpaceOnPrevious); if (previousSelected && squareCorner) { if (!string.IsNullOrEmpty(cornerModel)) { // Assez d'espace pour le caisson de coin -> "90". StartPoint = AiGet.WallStartPoint(Wallstring); EndPoint = AiGet.WallEndPoint(Wallstring); Delta = new() { StartPoint, EndPoint }; yield return StartCoroutine(AddBASECorners("90", Wallstring, Delta, SpaceOnWall, cornerModel)); } else if (SpaceOnPrevious >= 24f && !string.IsNullOrEmpty(_AI.CBDEADR)) { // Pas assez pour un vrai coin, mais >= 24" sur le mur précédent -> coin mort (B10) sur sa fin. StartPoint = AiGet.WallEndPoint(PreviousWallName); EndPoint = AiGet.WallStartPoint(PreviousWallName); Delta = new() { StartPoint, EndPoint }; yield return StartCoroutine(AddBASECorners("Dead_R", PreviousWallName, Delta, SpaceOnPrevious)); } else print("[AI] coin " + Wallstring + " : aucun coin ni coin mort possible (CBDEADR=\"" + _AI.CBDEADR + "\")."); } else print("[AI] coin " + Wallstring + " : écarté (mur précédent non sélectionné ou angle hors 89-91) -> la rangée B1 occupera l'angle."); } } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } //-------------------------------PÉNINSULE--------------------------------------------------------- // Débord de comptoir à l'arrière de la rangee avant quand la fonction est "Stool" : c'est l'assise des // tabourets. Zéro sans tabourets. const float PeninsulaStoolOverhang = 12f; // Champ de la recette ADN[42] ("H_gauche_arriere_droite_devant_...") qui porte le debord du // caisson de COIN : l'ARRIERE, comme pour les caissons de la rangee avant. // Cote AVANT d'un caisson de coin, selon sa main. Les quatre cotes se suivent — Left, // BackLeft, BackRight, Right — et le COTE AVANT est le dos tourne vers l'exterieur du L : // BackRight pour un coin "R", BackLeft pour un "L". static int CornerAvantIndex(string hand) => hand == "R" ? CornerBackRightIndex : CornerBackLeftIndex; // Les QUATRE cotes d'un caisson de coin — "Left", "BackLeft", "BackRight", "Right" — // occupent les champs 1 a 4 de la recette de comptoir, apres la hauteur au champ 0. // La valeur validee "1.25_0_1_1_0_1.75_1.75_0" le confirme : les 1" tombent aux champs 2 et 3, // soit les deux DOS, ceux qui sont exposes quand le coin ne touche rien. // Les champs 5 et 6 sont les deux segments de FACADE ; j'y avais mis le debord d'assise, et // avant cela au champ 4 : ni l'un ni l'autre n'est le cote qui longe la section disponible. const int CornerLeftIndex = 1; const int CornerBackLeftIndex = 2; const int CornerBackRightIndex = 3; const int CornerRightIndex = 4; // Portions de mur reservees a la projection du comptoir, en distance depuis le debut du mur. // La rangee basse ne doit RIEN y poser : un caisson de 12" s'y logeait sous le debord. static readonly Dictionary> _peninsulaReserved = new(); // Reserve les backProj pouces entre le bord de la zone remplissable et le meuble. static void ReservePeninsulaStrip(string wall, Vector3 anchor, Vector3 along, float backProj) { if (backProj <= 0f) return; string w = wall.Replace("w", "m"); Vector3 start = AiGet.WallStartPoint(w); Vector3 dir = (AiGet.WallEndPoint(w) - start).normalized; float d0 = Vector3.Dot(anchor - start, dir); float d1 = d0 + backProj * Vector3.Dot(along, dir); if (!_peninsulaReserved.TryGetValue(w, out var spans)) { spans = new List<(float s, float e)>(); _peninsulaReserved[w] = spans; } spans.Add((Mathf.Min(d0, d1), Mathf.Max(d0, d1))); print("[AI] Peninsule : " + backProj + " reserves sur " + w + " de " + Mathf.Min(d0, d1) + " a " + Mathf.Max(d0, d1) + " pour la projection du comptoir."); } // La travee [a,b] tombe-t-elle dans une reserve de projection ? Vrai des que la moitie de la // travee est couverte : c'est le cas de la travee de 12" laissee devant le caisson de coin. static bool InPeninsulaReserve(string wall, Vector3 a, Vector3 b) { string w = wall.Replace("w", "m"); if (!_peninsulaReserved.TryGetValue(w, out var spans) || spans.Count == 0) return false; Vector3 start = AiGet.WallStartPoint(w); Vector3 dir = (AiGet.WallEndPoint(w) - start).normalized; float da = Vector3.Dot(a - start, dir); float db = Vector3.Dot(b - start, dir); float s0 = Mathf.Min(da, db), e0 = Mathf.Max(da, db); float len = e0 - s0; if (len <= 0.01f) return false; foreach ((float s, float e) r in spans) { float overlap = Mathf.Min(e0, r.e) - Mathf.Max(s0, r.s); if (overlap > 0f && overlap >= len * 0.5f) return true; } return false; } // Noms des caissons de la RANGEE AVANT. Le lave-vaisselle les préfère à un caisson de la rangée murale : // dans une péninsule c'est là qu'il va, et ça évite qu'il vienne se placer contre l'évier. static readonly List _peninsulaRowCabs = new(); // Noms des caissons du RETOUR du L, a part de la rangee avant : c'est cette rangee-la qui porte les // tabourets quand la fonction est "StoolR". static readonly List _peninsulaReturnCabs = new(); // Noms des deux caissons de COIN de la peninsule. Les tabourets s'en servent pour borner // l'assise : ni l'un ni l'autre n'apparait dans la rangee que reconstruit CoroutineAddStools. static readonly List _peninsulaCornerCabs = new(); // Main reellement posee sur le premier coin : le second en prend l'inverse. static string _peninsulaFirstCornerHand = "L"; // Emplacement candidat pour une COLONNE. Ils sont tous collectés avant d'être servis : la // péninsule doit pouvoir choisir le bout de mur le plus dégagé, ce qui suppose de les connaître // tous — impossible en posant au fil de l'eau. class TallSpot { public string Wall; public List Delta; public string Model; public float Available; } static readonly List _tallSpots = new(); static void AddTallSpot(string wall, List delta, string model, float available) { _tallSpots.Add(new TallSpot { Wall = wall, Delta = delta, Model = model, Available = available }); } void DispatchTallSpots() { foreach (TallSpot s in _tallSpots) StartCoroutine(Add_TallCabinetonWall(s.Wall, s.Delta, s.Model)); _tallSpots.Clear(); } //--- Recherche d'emplacement ------------------------------------------------------------------- // Allee libre exigee au bout de la rangee avant et sur sa face ouverte. const float PENINSULA_MIN_AISLE = 36f; // Borne de la rangee avant, en nombre de caissons de base : au-dela ce n'est plus une peninsule. const int PENINSULA_MAX_CABS = 3; // Degagement minimal exige par un coin 90 (_AI.CB90 / _AI.CB9036) : la rangee qui repart sur // son cote doit mesurer au moins ca, sinon on n'accede pas au fond du caisson. En dessous, // c'est un COIN MORT (_AI.CBDEADL/R) qu'il faut poser. const float CORNER_L_MIN_RUN = 48f; //--- Peninsule en L ("Return") : regles propres, plus exigeantes que la peninsule droite. // Allee de 48" sur les trois cotes ouverts de l'espace disponible, au lieu des 36" d'une // peninsule droite : le L ferme davantage la circulation, il lui faut de quoi contourner. const float RETURN_CLEAR = 48f; // Degagement exige AU BOUT DU RETOUR du L, entre son extremite et la rangee murale. // C'est le bord de la ZONE DISPONIBLE, la meme regle que partout ailleurs : 48". // J'avais descendu cette valeur a 36, en lisant "66po de disponible" comme la place d'un // caisson de 24 plus un passage. C'etait une erreur de lecture : les 66" sont mesures depuis // la rangee murale, et la zone commence 48" plus loin — il reste donc 18" de caisson, pas 24. // Mur libre exige au point d'attache du premier coin. const float RETURN_MIN_WALL = 33f; // Degagement exige entre le DEBORD DE COMPTOIR et une ouverture, pour un "Return-Stool". // Remplace CornerOpeningSetback (12") : le comptoir des tabourets deborde vers l'ouverture et // 4" suffisent, la ou un caisson en demandait 12. const float RETURN_OPENING_SETBACK = 4f; // Borne haute du balayage : les deux coins delimitent l'espace et on remplit entre eux, donc // la longueur suit la place disponible au lieu d'etre plafonnee a 3 caissons. const int RETURN_MAX_CABS = 8; // Emplacement retenu : le caisson de coin s'adosse au mur en Anchor, la rangee avant part vers // l'interieur de la piece et la rangee murale se prolonge vers Along. class PeninsulaSpot { public string Wall; public Vector3 Anchor; public Vector3 Along; public bool AtWallStart; public float RowLen; // longueur utile de la rangee avant, allees deduites public PeninsulaCorner Corner; // modele de caisson de coin retenu public string Hand; // main du PREMIER coin, deduite du cote AVANT } // COTE INTERIEUR / COTE AVANT d'une zone accostee a un mur. // On regarde de part et d'autre de la portion touchee, le long du mur : le cote ou se trouvent // des objets (coin, tall, evier, cuisiniere, refrigerateur) est le COTE INTERIEUR ; l'autre, // celui qui ne bute sur rien, est le COTE AVANT. // Rend la direction du COTE AVANT, le long du mur. Vector3.zero si les deux cotes se valent. static Vector3 ZoneAvantDirection(string wallM, Vector3 wStart, Vector3 dir, float zu0, float zu1) { float nearLow = float.MaxValue; // objet le plus proche du cote des u decroissants float nearHigh = float.MaxValue; // ... et du cote des u croissants for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; GameObject o = Get.o2("SCENE", _G.OBJs[i][0]); if (!ZoneAnchorObject(o)) continue; float u = Vector3.Dot(o.transform.position - wStart, dir); if (u < zu0) nearLow = Mathf.Min(nearLow, zu0 - u); if (u > zu1) nearHigh = Mathf.Min(nearHigh, u - zu1); } bool low = nearLow < float.MaxValue; bool high = nearHigh < float.MaxValue; // Objets d'un seul cote : ce cote est l'INTERIEUR, l'AVANT est en face. if (low && !high) return dir; if (high && !low) return -dir; // Objets des deux cotes : le plus PROCHE fait l'interieur. if (low && high) return nearLow <= nearHigh ? dir : -dir; print("[AI] Peninsule " + wallM + " : aucun objet de part ni d'autre de la zone -> " + "cote Avant indetermine."); return Vector3.zero; } // Main du PREMIER caisson de coin, deduite du COTE AVANT. // Repere vectoriel, pas "la gauche du spectateur" : Cross(forward, up) donne, sur le plan, le // cote vers lequel un coin "R" ouvre. Si le COTE AVANT pointe dans ce sens, le coin est un // "R" ; sinon un "L". Vos deux schemas montrent exactement ces deux cas. static string CornerHandFromAvant(Vector3 forward, Vector3 avantDir) { Vector3 rightSide = Vector3.Cross(forward, Vector3.up).normalized; return Vector3.Dot(rightSide, avantDir) > 0f ? "R" : "L"; } // Cherche le meilleur emplacement de peninsule sur les murs selectionnes. // // Ancienne version : elle prenait le bout de mur ou il restait le plus de degagement HORS des // sections a caissons, sans jamais verifier que la rangee avant tenait dans la piece, et elle tournait // avant les colonnes, donc sur un sol presque vide. // // Nouvelle version : la peninsule prolonge la rangee murale par une de ses extremites, et on // mesure sur la carte d'occupation reelle si le corps du meuble ET ses allees tiennent. Le // rangee avant est aussi profond que la place le permet, dans la limite de PENINSULA_MAX_CABS. // Modeles de caisson de coin envisageables pour une peninsule, du plus profond au moins // profond. Le coin en L (CB9036/CB90) occupe 36" dans les deux sens ; le COIN MORT // (CBDEADL/CBDEADR) n'a que la profondeur d'un caisson de base et se pose comme un caisson de // rangee, donc il passe la ou le L ne passe pas. class PeninsulaCorner { public string Id; public bool Dead; public float Depth; // avancee dans la piece public float Wide; // emprise le long du mur public bool Turned; // coin mort pose EN TRAVERS : largeur et profondeur echangees } // Rotation supplementaire d'un coin mort pose en travers. Une seule constante a inverser si le // caisson ressort du mauvais cote — la geometrie est la meme a 90 pres, seule la face aveugle // change de bord. const float DeadCornerTurn = 90f; // Main du caisson de coin, donnee par l'OBJET LE PLUS PROCHE sur le meme mur : // objet a sa DROITE (plus loin le long du mur) -> "L" // objet a sa GAUCHE (plus pres du depart) -> "R" // Tous les objets du mur comptent, pas seulement four/frigo/evier comme dans // HandleSideFromReferences. Retourne "" si le mur est vide : l'appelant garde son defaut. static string CornerHandFromNearest(string wall, Vector3 pos) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); // DROITE au sens du SPECTATEUR qui regarde le mur depuis la piece — PAS la droite du // caisson, qui est l'oppose. C'est la source de toutes les inversions precedentes : les // deux reperes sont valables et donnent des reponses contraires. // into = AiGet.FowardbyWall = -transform.forward -> du mur vers la piece // droite du CAISSON (adosse au mur, regardant la piece) = Cross(up, into) // droite du SPECTATEUR (dans la piece, regardant le mur) = Cross(into, up), l'oppose // Verifie sur un cas reel : mur m3 (x=-96, into=+X), coin a z=-32, evier a z=44. Le // spectateur voit l'evier a SA DROITE ; Cross(into, up) = +Z le classe bien a droite. Vector3 into = AiGet.FowardbyWall(wnum); into = new Vector3(into.x, 0f, into.z).normalized; Vector3 dir = Vector3.Cross(into, Vector3.up).normalized; float cx = Vector3.Dot(new Vector3(pos.x - start.x, 0f, pos.z - start.z), dir); float best = float.MaxValue; string hand = ""; string refName = ""; int seen = 0; // objets du mur examines int kept = 0; // ceux qui passent le filtre caisson bas / four / frigo for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; seen++; // Seuls comptent les CAISSONS BAS, la CUISINIERE et le FRIGO. Les colonnes, les // caissons hauts, les panneaux et les ouvertures ne donnent pas la main du coin. string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; bool isBase = o[2] == "Cabinet" && !string.IsNullOrEmpty(o[4]) && o[4][0] == 'B'; if (!isBase && n4 != "stov" && n4 != "frid") continue; kept++; float rx = Vector3.Dot(new Vector3(DOIT.ConvertStringToNumber(o[15]) - start.x, 0f, DOIT.ConvertStringToNumber(o[17]) - start.z), dir); float d = Mathf.Abs(cx - rx); if (d >= best) continue; best = d; refName = o[0]; // Objet le plus proche a DROITE -> "L", a GAUCHE -> "R". // Le SECOND coin prend l'inverse : avec l'evier a droite du premier, on obtient // coin 1 = "L" et coin 2 = "R", la configuration attendue. hand = rx > cx ? "L" : "R"; } // Trace systematique, y compris quand rien n'est trouve : ce chemin muet renvoyait "" // et l'appelant retombait sur atWallStart ? "L" : "R" sans que rien ne le signale. if (hand != "") { print("[AI] Peninsule main du coin : " + refName + " (coin a " + cx + " sur l'axe droite=" + dir.x.ToString("0.0") + "," + dir.z.ToString("0.0") + " ; objet a " + best + " " + (hand == "L" ? "A DROITE" : "A GAUCHE") + ") -> " + hand + " [" + seen + " objets sur le mur, " + kept + " retenus]"); } else { print("[AI] Peninsule main du coin : AUCUNE reference sur " + wnum + " [" + seen + " objets sur le mur, " + kept + " retenus] -> repli sur le bout de mur."); } return hand; } // secondCorner : le coin du BOUT de la peninsule en L. Il prend la main OPPOSEE au premier, le L se // refermant — et donc aussi le modele de coin mort oppose. static List PeninsulaCornerModels(bool atWallStart, float baseDepth, bool secondCorner = false) { var list = new List(); // COINS 90, carres : DEUX references distinctes au catalogue, CB90 (33x33) et CB9036 // (36x36). AVANT, CB90 n'etait essaye qu'en l'ABSENCE de CB9036 — le 33" ne pouvait donc // jamais l'emporter, alors qu'il remplit parfois mieux le COTE BOUT que le 36". foreach (string id in new[] { _AI.CB9036, _AI.CB90 }) { if (string.IsNullOrEmpty(id) || list.Exists(c => c.Id == id)) continue; float d = UIT.ValueFloat(Library.Cabinet, id, Header.C); if (d <= 0f) d = SafeCabinetWidth(id); if (d <= 0f) continue; list.Add(new PeninsulaCorner { Id = id, Dead = false, Depth = d, Wide = d }); } // Coin mort. Le premier coin prend CBDEADR quand la peninsule OUVRE le mur (DEBUT) et // CBDEADL quand elle le ferme (FIN) — l'inverse de ce que j'avais ecrit : le coin mort // sortait du mauvais cote. Le SECOND coin prend l'autre modele, comme il prend l'autre main. bool openSide = secondCorner ? !atWallStart : atWallStart; string dead = openSide ? _AI.CBDEADR : _AI.CBDEADL; if (!string.IsNullOrEmpty(dead)) { // Le coin mort est RECTANGULAIRE (36 x 24), et il se pose de DEUX facons : // a plat -> 36 le long de la rangee, 24 d'avancee ; // en travers -> tourne de 90, donc 24 le long de la rangee et 36 d'avancee. // Les deux sont proposees : selon la place restante, l'une remplit mieux le COTE BOUT // que l'autre, et c'est la comparaison des totaux qui tranche. float w = SafeCabinetWidth(dead); if (w <= 0f) w = 36f; float d = UIT.ValueFloat(Library.Cabinet, dead, Header.C); if (d <= 0f) d = baseDepth; list.Add(new PeninsulaCorner { Id = dead, Dead = true, Depth = d, Wide = w }); list.Add(new PeninsulaCorner { Id = dead, Dead = true, Depth = w, Wide = d, Turned = true }); } return list; } static bool FindPeninsulaSpot(out PeninsulaSpot best) { best = null; if (WallSelected == null || WallSelected.Count == 0) { print("[AI] Peninsule : aucun mur selectionne."); return false; } float baseDepth = _AI.BaseDepth + _AI.Door1Thickness; float baseWide = SafeCabinetWidth(_AI.BASE); if (baseWide <= 0f) baseWide = 24f; if (PeninsulaCornerModels(true, baseDepth).Count == 0) { print("[AI] Peninsule : aucun caisson de coin au catalogue (CB9036/CB90/CBDEAD vides)."); return false; } // Debord de comptoir des tabourets. Il se prend DANS la zone remplissable, devant le // caisson de coin : le meuble recule d'autant vers le reste de la rangee et le comptoir // occupe seul les 12" liberes. Le prendre au-dela du bord faisait deborder le comptoir // sur la rangee murale ou sur une ouverture. float backProj = RowSeatProjection(); // Carte d'occupation DURE : uniquement l'exterieur du polygone de la piece et l'emprise // reelle des objets, sans les degagements de l'ilot. La peninsule est ADOSSEE au mur : avec // l'inset de 36" de la carte d'ilot, aucun emplacement contre un mur n'aurait ete libre. // Les allees sont verifiees explicitement plus bas, la ou elles ont un sens. BuildPeninsulaZone(out bool[,] occ, out int cols, out int rows, out float ox, out float oz); print("[AI] Peninsule : recherche sur " + WallSelected.Count + " mur(s), caisson " + _AI.BASE + " (" + baseWide + "), debord tabourets " + backProj + ", grille " + cols + "x" + rows + " depuis (" + ox + "," + oz + ")."); // Resume par mur, remonte dans la ligne d'abandon : un collage partiel du journal suffit // alors a localiser la contrainte qui bloque. var diag = new System.Text.StringBuilder(); foreach (string wall in WallSelected) { string wnum = wall.Replace("m", "w"); Vector3 wStart = AiGet.WallStartPoint(wnum); Vector3 wEnd = AiGet.WallEndPoint(wnum); Vector3 dir = (wEnd - wStart).normalized; float wallLen = Vector3.Distance(wStart, wEnd); GameObject wobj = Get.o1(wall); if (wobj == null) { print("[AI] Peninsule : mur " + wall + " introuvable dans la scene."); continue; } Vector3 forward = -wobj.transform.forward.normalized; // Zone REELLEMENT remplissable du mur = ce que voit la rangee basse : SetDeltaPoint // (reserves de coin, ouvertures voisines) puis AllowedSubDeltas (sections ecartees dans // l'UI). La peninsule s'attache a l'un de ses deux bords, jamais ailleurs : c'est ce qui // la raccorde aux armoires du mur sans laisser de trou. List obs = AiGet.AllObjectsOnWall(wall, "All", "All", _AI.FloorLine + 10, _AI.FloorLine); List usable = SetDeltaPoint(wall, wStart, wEnd, "Base", obs); float uStart = Vector3.Dot(usable[0] - wStart, dir); float uEnd = Vector3.Dot(usable[1] - wStart, dir); List> stretches = AllowedSubDeltas(wall, wStart, wEnd, false); if (stretches.Count > 0) { uStart = Mathf.Max(uStart, Vector3.Dot(stretches[0][0] - wStart, dir)); uEnd = Mathf.Min(uEnd, Vector3.Dot(stretches[^1][1] - wStart, dir)); } // "Return-Stool" : le comptoir deborde vers l'ouverture et la regle passe de // CornerOpeningSetback a RETURN_OPENING_SETBACK. On rend la difference a la zone, aux // deux bouts ; les tests d'espace qui suivent valident ou rejettent ce gain. // Uniquement quand le comptoir de la RANGEE AVANT deborde vers l'ouverture : en "StoolR" // l'assise est au bout, le coin 1 ne porte qu'une projection de 1" et n'a aucune raison // de gagner ces 8". if (IslandTypeIs("Return") && StoolsAlongAisle()) { float gain = CornerOpeningSetback - RETURN_OPENING_SETBACK; float u0 = uStart, u1 = uEnd; uStart = Mathf.Max(0f, uStart - gain); uEnd = Mathf.Min(wallLen, uEnd + gain); print("[AI] Peninsule en L " + wall + " : degagement d'ouverture " + CornerOpeningSetback + " -> " + RETURN_OPENING_SETBACK + ", zone " + u0 + "-" + u1 + " elargie a " + uStart + "-" + uEnd + "."); } // LE MUR RETENU EST CELUI QUE LA ZONE TOUCHE, et la portion touchee devient le // COTE MUR. Ailleurs la zone s'ecarte du mur de plus d'une profondeur de caisson : une // peninsule adossee la partirait deja hors zone. On intersecte donc la travee // remplissable avec la portion touchee, et un mur que la zone n'atteint pas est ecarte. if (!ZoneTouchSpan(occ, cols, rows, ox, oz, wStart, dir, forward, wallLen, baseDepth, out float zu0, out float zu1)) { print("[AI] Peninsule " + wall + " : la zone ne touche pas ce mur -> ecarte."); diag.Append(wall).Append(" zone hors mur ; "); continue; } float pu0 = uStart, pu1 = uEnd; uStart = Mathf.Max(uStart, zu0); uEnd = Mathf.Min(uEnd, zu1); if (uEnd - uStart < 0.01f) { print("[AI] Peninsule " + wall + " : travee remplissable " + pu0 + "-" + pu1 + " et portion touchee par la zone " + zu0 + "-" + zu1 + " disjointes -> ecarte."); diag.Append(wall).Append(" travee/zone disjointes ; "); continue; } print("[AI] Peninsule " + wall + " : la zone touche le mur sur " + zu0 + "-" + zu1 + " -> travee ramenee de " + pu0 + "-" + pu1 + " a " + uStart + "-" + uEnd + "."); print("[AI] Peninsule " + wall + " : zone remplissable de " + uStart + " a " + uEnd + " (mur de " + wallLen + "), depart " + wStart.x.ToString("0") + "," + wStart.z.ToString("0") + " dir " + dir.x.ToString("0.0") + "," + dir.z.ToString("0.0") + " fwd " + forward.x.ToString("0.0") + "," + forward.z.ToString("0.0") + "."); float zoneLen = uEnd - uStart; diag.Append(wall).Append(" zone ").Append(uStart).Append("-").Append(uEnd) .Append(" (long ").Append(zoneLen).Append(" / mur ").Append(wallLen).Append(") ; "); // Le coin 1 se pose au bout du COTE MUR situe du COTE AVANT — celui qui ne bute sur // aucun objet. AVANT, les deux bouts etaient essayes et le meilleur remplissage // tranchait ; la regle dit maintenant lequel des deux, et la main qui va avec. Vector3 avantDir = ZoneAvantDirection(wall, wStart, dir, zu0, zu1); if (avantDir == Vector3.zero) avantDir = dir; // repli : bout de fin, comme avant bool avantAtEnd = Vector3.Dot(avantDir, dir) > 0f; float anchorU = avantAtEnd ? uEnd : uStart; string hand = CornerHandFromAvant(forward, avantDir); print("[AI] Peninsule " + wall + " : cote AVANT vers " + avantDir.x.ToString("0.0") + "," + avantDir.z.ToString("0.0") + " -> coin 1 au bout " + (avantAtEnd ? "FIN" : "DEBUT") + " (u=" + anchorU + "), main " + hand + "."); EvaluatePeninsulaSpot(wall, wStart, dir, forward, anchorU, !avantAtEnd, zoneLen, backProj, baseDepth, baseWide, occ, cols, rows, ox, oz, hand, ref best); } if (best == null) print("[AI] Peninsule : zones examinees -> " + diag.ToString()); return best != null; } // Decalage minimal, le long de 'along', pour que TOUTE l'emprise passee en 'span' tombe dans // une section autorisee. Rend -1 si aucun decalage jusqu'a 'limit' n'y parvient. // // Le test de section ne portait que sur UN POINT — l'aplomb du caisson. L'emprise reelle du // coin 1 est plus large que lui : son comptoir deborde de backProj vers l'ouverture. Et en // "Return-Stool" la zone est justement rendue plus large de 8" de ce cote // (CornerOpeningSetback -> RETURN_OPENING_SETBACK) : le debord se logeait tout entier dans ce // gain, donc devant l'ouverture, pendant que le point teste, lui, restait dans une section // valide. D'ou une projection qui passait devant l'ouverture sans que rien ne la refuse. static float SectionShiftForSpan(string wall, Vector3 anchor, Vector3 along, float span, float limit) { const float step = 1f; for (float shift = 0f; shift <= limit + 0.01f; shift += step) { bool ok = true; for (float u = 0f; u < span; u += step) { if (!SectionAllowed(wall, anchor + along * (shift + u))) { ok = false; break; } } // Le bout exact de l'emprise, que le pas d'echantillonnage saute presque toujours. if (ok && !SectionAllowed(wall, anchor + along * (shift + span - 0.01f))) ok = false; if (ok) return shift; } return -1f; } // Teste un bord de rangee, pour chaque modele de coin envisageable. Retient l'emplacement s'il // loge une rangee avant plus profond que le meilleur connu. // uEdge = distance du bord depuis le debut du mur // zoneLen = longueur de la zone remplissable du mur static void EvaluatePeninsulaSpot(string wall, Vector3 wStart, Vector3 dir, Vector3 forward, float uEdge, bool atWallStart, float zoneLen, float backProj, float baseDepth, float baseWide, bool[,] occ, int cols, int rows, float ox, float oz, string hand, ref PeninsulaSpot best) { Vector3 anchor = wStart + dir * uEdge; // along pointe vers l'INTERIEUR de la zone remplissable : le caisson de coin s'y pose, dans // la continuite de la rangee, et seul la rangee avant part dans la piece. Vector3 along = atWallStart ? dir : -dir; string side = atWallStart ? "DEBUT" : "FIN"; // Le meuble RECULE de backProj dans la zone : les 12" liberes devant lui sont l'assise des // tabourets, occupee par le seul comptoir. // Le test de section se fait plus bas, par MODELE DE COIN, sur l'emprise complete // (debord + largeur du caisson) : elle depend de pc.Wide, et un test ponctuel pose ici // rejetait l'emplacement avant que le decalage n'ait pu le sauver. // Le L est plus exigeant que la peninsule droite : 48" d'allee et un mur d'attache // d'au moins 33". Le nombre de caissons n'est plus plafonne a 3 — les deux coins bornent // l'espace et la rangee avant remplit ce qui reste entre eux. bool isReturn = IslandTypeIs("Return"); // L'allee n'est plus testee ici : la ZONE la porte deja, mur par mur, avec ses bandes de // 48" et 72". La reprendre bande par bande la compterait deux fois et ne laisserait plus // aucun emplacement. int maxCabs = isReturn ? RETURN_MAX_CABS : PENINSULA_MAX_CABS; if (isReturn && zoneLen < RETURN_MIN_WALL) { print("[AI] Peninsule en L " + wall + " cote " + side + " : mur disponible de " + zoneLen + " < " + RETURN_MIN_WALL + " -> ecarte."); return; } foreach (PeninsulaCorner pc in PeninsulaCornerModels(atWallStart, baseDepth)) { // EMPRISE TOTALE du coin le long du mur : le debord du comptoir PLUS la largeur du // caisson. C'est elle qui ne doit pas passer devant l'ouverture, pas le seul caisson. float span = backProj + pc.Wide; // La zone doit loger le recul ET l'emprise du coin. if (zoneLen < span - 0.01f) { print("[AI] Peninsule " + wall + " cote " + side + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + " : zone de " + zoneLen + " trop courte pour le recul " + backProj + " + le coin " + pc.Wide + "."); continue; } // On glisse le long de la zone jusqu'a ce que l'emprise entiere tienne dans une // section autorisee. Le caisson se posera ensuite a une demi-largeur de la, comme // avant : c'est son CENTRE que Set_TemporySC attend. float shift = SectionShiftForSpan(wall, anchor, along, span, zoneLen - span); if (shift < 0f) { print("[AI] Peninsule " + wall + " cote " + side + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + " : emprise de " + span + " (debord " + backProj + " + caisson " + pc.Wide + ") ne tient dans aucune section de la zone -> ecarte."); continue; } if (shift > 0f) print("[AI] Peninsule " + wall + " cote " + side + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + " : emprise de " + span + " decalee de " + shift + " pour ne pas passer devant l'ouverture."); Vector3 anchorPc = anchor + along * shift; Vector3 cabAnchorPc = anchorPc + along * backProj; for (int n = maxCabs; n >= 1; n--) { float rowLen = n * baseWide; float depthIn = pc.Depth + rowLen; // Un coin 90 n'a de sens que si la rangee posee sur son cote — ici la rangee avant — // laisse le degagement d'acces. Sinon on laisse la main au coin MORT, essaye // juste apres dans la liste des modeles. if (!pc.Dead && rowLen < CORNER_L_MIN_RUN) { print("[AI] Peninsule " + wall + " cote " + side + " coin L " + pc.Id + " : rangee avant de " + rowLen + " < " + CORNER_L_MIN_RUN + " de degagement -> coin mort a la place."); continue; } // Bande du CAISSON DE COIN : son emprise propre le long du mur, apres le recul. bool cornerFree = RectFree(occ, cols, rows, ox, oz, cabAnchorPc + along * (pc.Wide * 0.5f) + forward * (pc.Depth * 0.5f), forward, pc.Depth, along, pc.Wide, out Vector3 hitCorner); // RANGEE AVANT : la partie qui sort dans la piece, large d'un caisson de base. bool rowFree = RectFree(occ, cols, rows, ox, oz, cabAnchorPc + along * (baseDepth * 0.5f) + forward * (pc.Depth + rowLen * 0.5f), forward, rowLen, along, baseDepth, out Vector3 hitRow); // Allee au BOUT : on doit pouvoir contourner la peninsule. // Ce qu'il faut au-dela de la rangee avant. Pour une peninsule DROITE : l'allee seule. // Pour un RETURN : la place du SECOND COIN puis son allee — sinon la rangee avant prend // toute la profondeur, le coin 2 n'a plus ou aller et le L se reduit a une // peninsule droite. On reserve la profondeur du coin le moins encombrant (le coin // mort, baseDepth) pour ne pas ecarter des emplacements qu'il pourrait occuper ; // AddPeninsulaReturn re-teste ensuite le modele reellement posable. // On y ajoute backProj : le comptoir du coin 2 deborde d'autant sur sa face // exposee, comme celui de la rangee avant. Sans cette reserve la rangee avant prenait toute la // profondeur disponible et le debord du L n'avait plus ou aller — c'est la // projection de comptoir a prevoir comme sur la peninsule. // Il reste a reserver la PLACE DU SECOND COIN au-dela de la rangee avant — ca, // la zone ne le sait pas : c'est du meuble, pas du degagement. float beyond = isReturn ? baseDepth + backProj : 0f; Vector3 hitTip = Vector3.zero; bool tipFree = beyond <= 0f || RectFree(occ, cols, rows, ox, oz, cabAnchorPc + along * (baseDepth * 0.5f) + forward * (depthIn + beyond * 0.5f), forward, beyond, along, baseDepth, out hitTip); // ASSISE DES TABOURETS : les backProj pouces liberes entre le bord de la zone et // le meuble. On ne la teste que sur la profondeur du coin, la ou elle longe la // rangee murale ; plus loin elle longe l'allee, vide par construction. // Declare AVANT le ||, sinon le court-circuit laisse hitProj non assigne (CS0165). Vector3 hitProj = Vector3.zero; bool projFree = backProj <= 0f || RectFree(occ, cols, rows, ox, oz, anchorPc + along * (backProj * 0.5f) + forward * (pc.Depth * 0.5f), forward, pc.Depth, along, backProj, out hitProj); if (!cornerFree || !rowFree || !tipFree || !projFree) { string why = ""; if (!cornerFree) why += "coin bloque par " + BlockerAt(hitCorner) + " ; "; if (!rowFree) why += "rangee avant bloque par " + BlockerAt(hitRow) + " ; "; if (!tipFree) why += (isReturn ? "coin 2 + allee de bout bloques par " : "allee de bout bloquee par ") + BlockerAt(hitTip) + " ; "; if (!projFree) why += "assise des tabourets bloquee par " + BlockerAt(hitProj) + " ; "; print("[AI] Peninsule " + wall + " cote " + side + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + " : " + n + " caisson(s) refuse(s) (avancee " + depthIn + ") - " + why); continue; } // Ce modele de coin a donne son meilleur rangee avant : on le retient s'il bat le // meilleur connu, puis on passe au modele suivant. Le coin MORT est moins profond // que le L, il laisse donc souvent la place a une rangee avant plus long — le comparer // vaut la peine au lieu de s'arreter au premier modele qui passe. if (best == null || rowLen > best.RowLen) { best = new PeninsulaSpot { Wall = wall, Anchor = anchorPc, Along = along, AtWallStart = atWallStart, RowLen = rowLen, Corner = pc, Hand = hand }; print("[AI] Peninsule candidat " + wall + " cote " + side + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + " : rangee avant de " + rowLen + " (" + n + " caissons), avancee " + depthIn + "."); } break; } } if (best != null) return; print("[AI] Peninsule " + wall + " cote " + side + " : aucun modele de coin ne loge une rangee avant, meme d'un seul caisson."); } // Toutes les cellules du rectangle (centre + deux demi-axes) sont-elles libres ? // Echantillonnage au CENTRE des cellules : un rectangle adosse au mur ne teste donc pas la // ligne du mur elle-meme, qui est hors polygone. static bool RectFree(bool[,] occ, int cols, int rows, float ox, float oz, Vector3 center, Vector3 axisA, float lenA, Vector3 axisB, float lenB) => RectFree(occ, cols, rows, ox, oz, center, axisA, lenA, axisB, lenB, out _); static bool RectFree(bool[,] occ, int cols, int rows, float ox, float oz, Vector3 center, Vector3 axisA, float lenA, Vector3 axisB, float lenB, out Vector3 hit) { hit = center; float step = ISLAND_GRID_STEP; int na = Mathf.Max(1, Mathf.CeilToInt(lenA / step)); int nb = Mathf.Max(1, Mathf.CeilToInt(lenB / step)); for (int i = 0; i < na; i++) { float a = -lenA * 0.5f + (i + 0.5f) * (lenA / na); for (int j = 0; j < nb; j++) { float b = -lenB * 0.5f + (j + 0.5f) * (lenB / nb); Vector3 pt = center + axisA * a + axisB * b; int c = Mathf.FloorToInt((pt.x - ox) / step); int r = Mathf.FloorToInt((pt.z - oz) / step); if (c < 0 || r < 0 || c >= cols || r >= rows) { hit = pt; return false; } if (occ[r, c]) { hit = pt; return false; } } } return true; } // Diagnostic : nomme ce qui occupe un point du sol. Reprend le calcul d'emprise de // BuildOccupancyGrid (AABB du rectangle tourne), sans tampon. static string BlockerAt(Vector3 pt) { GetRoomBoundary(out List poly, out _, out _, out _); if (poly.Count >= 3 && !PointInPolygon(new Vector2(pt.x, pt.z), poly)) return "HORS PIECE (" + pt.x.ToString("0") + "," + pt.z.ToString("0") + ")"; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (IsWallOpening(o)) continue; if (o[0].Length >= 4) { string n4 = o[0][..4]; if (n4 == "lamp" || n4 == "stoo" || n4 == "hood") continue; } if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; float cx = DOIT.ConvertStringToNumber(o[15]); float cz = DOIT.ConvertStringToNumber(o[17]); float w = DOIT.ConvertStringToNumber(o[6]); float d = DOIT.ConvertStringToNumber(o[8]); if (w <= 0f || d <= 0f) continue; float ang = DOIT.ConvertStringToNumber(o[19]) * Mathf.Deg2Rad; float exX = Mathf.Abs(Mathf.Cos(ang)) * w * 0.5f + Mathf.Abs(Mathf.Sin(ang)) * d * 0.5f; float exZ = Mathf.Abs(Mathf.Sin(ang)) * w * 0.5f + Mathf.Abs(Mathf.Cos(ang)) * d * 0.5f; if (Mathf.Abs(pt.x - cx) <= exX && Mathf.Abs(pt.z - cz) <= exZ) return o[0] + "/" + o[1] + " (" + o[2] + ", " + w + "x" + d + " en " + cx.ToString("0") + "," + cz.ToString("0") + ")"; } return "rien d'identifie (" + pt.x.ToString("0") + "," + pt.z.ToString("0") + ")"; } // Pose la peninsule a l'emplacement mesure par FindPeninsulaSpot. IEnumerator CoroutineAddPeninsula() { if (!WantsPeninsula()) yield break; if (!FindPeninsulaSpot(out PeninsulaSpot spot)) { bool ret = IslandTypeIs("Return"); print("[AI] Peninsule : aucun emplacement -> abandon." + " Type=" + IslandType() + " tabourets=" + WantsStools() + " | contraintes : recul " + RowSeatProjection() + ", allee " + (ret ? RETURN_CLEAR : PENINSULA_MIN_AISLE) + (ret ? ", mur mini " + RETURN_MIN_WALL + ", reserve coin 2 activee" : "") + ", coin 90 exige " + CORNER_L_MIN_RUN + " de rangee."); yield break; } // Composition de la rangee avant avec des largeurs variees, exactement comme la rangee d'ilot. // L'evier n'y va que s'il n'y en a pas deja au mur : avec "Peninsula-Sink", AddSinkCabinet // s'en est abstenu et c'est ici qu'il est pose. bool sinkInPeninsula = !SinkOnWallExists(); var rowCabs = BuildIslandCabinets(spot.RowLen, sinkInPeninsula); if (rowCabs.Count == 0) { print("[AI] Peninsule : aucun caisson ne tient dans une rangee avant de " + spot.RowLen + " -> abandon."); yield break; } print("[AI] Peninsule retenue sur " + spot.Wall + " cote " + (spot.AtWallStart ? "DEBUT" : "FIN") + " : coin " + (spot.Corner.Dead ? "MORT " : "L ") + spot.Corner.Id + ", " + rowCabs.Count + " caissons de rangee avant, evier " + (sinkInPeninsula ? "DANS" : "hors") + " la peninsule."); yield return StartCoroutine(AddPeninsulaAt(spot.Hand, spot.Wall, new List { spot.Anchor, spot.Anchor + spot.Along }, spot.AtWallStart, spot.Corner, rowCabs)); } // Pose une PÉNINSULE à l'emplacement mesuré par FindPeninsulaSpot : un caisson de COIN contre le // mur, dans la continuité de la rangée, puis les caissons de 'rowCabs' alignés sur sa face qui // n'est PAS au mur, donc partant vers l'intérieur de la pièce. // Delta[0] = point du mur où commence l'emplacement, Delta[1] donne le sens le long du mur. // "Return" (péninsule en L) réutilise la même amorce ; ce qui la distingue est la suite de la rangee avant, // à préciser — voir la note remontée avec ce changement. // atWallStart : le dégagement retenu est du côté du DÉBUT du mur. Le caisson de coin est alors un // "L" ; côté FIN, un "R". IEnumerator AddPeninsulaAt(string hand, string WallNumber, List Delta, bool atWallStart, PeninsulaCorner pc, List<(string id, float w)> rowCabs) { // La SECTION se teste a l'aplomb du MEUBLE, pas de l'ancre — meme correction que dans // EvaluatePeninsulaSpot. Delta[0] est le bord de la zone ; le meuble recule de backProj // derriere lui, l'intervalle ne portant que la projection du comptoir. En "Return-Stool" // l'ancre recule en plus de RETURN_OPENING_SETBACK dans la zone de l'ouverture : la tester // la, c'etait retenir l'emplacement dans la recherche puis le rejeter ici. // Les deux valeurs sont recalculees au propre plus bas ; ici on en a besoin AVANT // _aiBusy++, une sortie apres l'incrementation laisserait AiProcess attendre pour rien. Vector3 sectionProbeDir = (Delta[1] - Delta[0]).normalized; float sectionProbeOff = RowSeatProjection(); if (!SectionAllowed(WallNumber, Delta[0] + sectionProbeDir * (sectionProbeOff + 0.5f))) { print("[AI] Peninsule " + WallNumber + " : section non autorisee a l'aplomb du meuble -> abandon."); yield break; } if (pc == null || string.IsNullOrEmpty(pc.Id)) { print("[AI] Peninsule " + WallNumber + " : aucun caisson de coin retenu -> abandon."); yield break; } string corner = pc.Id; _aiBusy++; // Delta[0] = bord de la SECTION, PAS un coin de mur : le caisson de coin ne s'encastre pas // entre deux murs, il ouvre la rangée. Un seul mur est derrière lui, sa face de rangee avant est // exposée dans la pièce. Vector3 along = (Delta[1] - Delta[0]).normalized; // vers l'INTÉRIEUR de la section Vector3 forward = -Get.o1(WallNumber).transform.forward.normalized; // vers l'intérieur de la pièce // Angle du MUR, pas celui de Delta : 'along' change de sens selon le bout retenu, l'orientation // du caisson ne doit pas suivre. C'est ADN[23] qui porte la main, "L" au début, "R" à la fin. float wallAngle = GetAngleByWall(WallNumber); // 1) Caisson de coin : dos au mur, son bord côté ouvert pile sur Delta[0]. // Coin en L -> il occupe [0, 36] le long du mur ET vers la pièce. // COIN MORT -> il se pose comme un caisson de rangée : sa largeur le long du mur, la // profondeur d'un caisson de base vers la pièce. La rangee avant part alors plus tôt. float cornerDepth = pc.Depth; float cornerWide = pc.Wide; // Main du caisson de coin : donnee par l'OBJET LE PLUS PROCHE du meme mur — a sa droite // -> "L", a sa gauche -> "R". Repli sur l'ancienne regle de bout de mur si le mur est vide. // Reaffecte apres Set_TemporySC, qui pourrait sinon recalculer un cote, comme le fait // AddBASECorners("Dead_R"). // Main donnee par la RECHERCHE, deduite du cote AVANT de la zone. La regle de l'objet le // plus proche (CornerHandFromNearest) raisonnait sur le voisin immediat sur le mur ; elle // ne dit rien de la zone et se trompait des que le voisin n'etait pas du cote de l'ouverture. string cornerFlip = hand; if (string.IsNullOrEmpty(cornerFlip)) cornerFlip = CornerHandFromNearest(WallNumber, Delta[0]); if (string.IsNullOrEmpty(cornerFlip)) cornerFlip = atWallStart ? "L" : "R"; _peninsulaFirstCornerHand = cornerFlip; // Fonction "Stool" : le comptoir déborde sur la face ouverte de la péninsule pour asseoir // les tabourets. Cette place se prend DANS la zone remplissable : tout le meuble recule de // backProj vers le reste de la rangée, et les 12" libérés devant lui ne portent que le // comptoir. Le coin ET la rangee avant reculent ensemble — les décaler l'un par rapport à l'autre // laissait un vide de 12" au bord de la rangée, comblé par un B1 que le coin recouvrait. // Le comptoir deborde de 12" avec tabourets, de 1" sans — et le meuble RECULE d'autant, // pour que ce debord ait sa place au lieu de coiffer la rangee murale. Une seule valeur // pour les deux : le recul EST l'epaisseur du debord. float backProj = RowSeatProjection(); // Le meuble RECULE de backProj le long du mur : les 12" liberes devant lui accueillent // le seul comptoir, pour asseoir les tabourets. Coin ET rangee avant reculent ensemble, sinon // ils se retrouvent decales l'un par rapport a l'autre. Vector3 cabAnchor = Delta[0] + along * backProj; ReservePeninsulaStrip(WallNumber, Delta[0], along, backProj); Vector3 pos = cabAnchor + along * (cornerWide * 0.5f) + forward * (cornerDepth * 0.5f); pos.y = _AI.BaseHeightPosition; // Pose sur la main de DEPART, pas sur celle voulue : FlipCornerTo a besoin d'un point de // reference connu pour savoir s'il doit inverser. Ecrire directement la main voulue rendait // la comparaison toujours vraie, et l'inversion ne partait jamais. string cornerBase = CornerBaseHand(pc); Set_TemporySC(corner, WallNumber, pos, wallAngle + (pc.Turned ? DeadCornerTurn : 0f), cornerBase); // Ligne de base d'un coin mort : le couple "R" + B10, comme // AddBASECorners("Dead_R"). FlipCornerTo l'amenera en "L" par // CONTROL.FlipCabinet si la main voulue differe. _G.SC[23] = cornerBase; // Un coin MORT est un caisson de rangée à bout aveugle. Sa MAIN se joue sur le code // modèle, pas sur ADN[23] : ConstructCORNERBASE ne lit jamais ADN[23], et // CONTROL.FlipCabinet inverse un coin en échangeant B9 <-> B10 (avec les profondeurs de // côté). "R" -> B10, comme AddBASECorners("Dead_R") ; "L" -> B9. // Poser B10 en dur laissait donc les deux coins en "R" quoi que calcule la main. if (pc.Dead) _G.SC[4] = "B10"; // ligne de base, comme AddBASECorners("Dead_R") _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; // Mêmes champs qu'AddBASECorners : un caisson de coin a DEUX faces de dosseret — ADN[55] et // ADN[56] — et deux profondeurs de côté. Sans ADN[56], DOBS.SplashL levait une // NullReferenceException sur un Split de null. _G.SC[55] = "0,0,0,0"; _G.SC[56] = "0,0,0,0"; _G.SC[28] = _AI.BaseDepth.ToSafeString(); _G.SC[29] = _AI.BaseDepth.ToSafeString(); CreateCAB.AddCab(); int cornerIdx = _G.OBJnum - 1; if (cornerIdx >= 0) _peninsulaCornerCabs.Add(_G.OBJs[cornerIdx][0]); yield return _waitForSecondsRealtime2; // MAIN du caisson de coin. Elle ne tient pas dans ADN[23] : ConstructCORNERBASE ne lit // jamais ce champ. L'inversion complete d'un coin — modele B9<->B10, profondeurs de cote // C[28]/C[29], champs C[78]/C[83], recettes des DEUX portes — est deja ecrite dans // CONTROL.FlipCabinet, celle qu'utilise le bouton FLIP. On pose le coin sur sa ligne de // base, puis on l'inverse par ce chemin eprouve si la main obtenue n'est pas la voulue. yield return FlipCornerTo(cornerIdx, cornerFlip, "coin 1"); // Débord du comptoir du COIN, au-delà de l'ancre, pour rejoindre le débord arrière du // rangee avant : les deux plans forment alors une seule surface continue. S'applique AUSSI sans // tabourets, avec la projection ordinaire de 1" — sinon la joue du coin restait à ras du // caisson pendant que la rangee avant débordait. // Écrit APRÈS coup : sur un caisson mural, CreateCAB prend ADN[42] dans la LIBRAIRIE et // ignore _G.SC[42]. // ADN[42] = "H_gauche_arrière_droite_devant_...". Sur un caisson de COIN, le débord se // porte sur l'ARRIÈRE (index 2), quelle que soit la main "L"/"R" du caisson — et non sur // gauche/droite comme je l'avais écrit : le comptoir sortait alors du mauvais côté. // CornerAvantIndex isole ce choix : une seule aide à changer si la main devait // finalement viser un autre champ. if (backProj > 0f && cornerIdx >= 0 && _G.OBJs[cornerIdx] != null) { string[] cc = _G.OBJs[cornerIdx]; // La recette de comptoir vient de la librairie et n'a pas toujours les 4 champs // attendus. AVANT, on renonçait alors SANS RIEN DIRE : le comptoir du coin restait à // ras, la rangee avant débordait de 12", et il restait une marche entre les deux plans. // Maintenant on repart d'une recette complète — la même que celle de la rangee avant — pour que // les deux surfaces se rejoignent dans tous les cas. if (string.IsNullOrEmpty(cc[42]) || cc[42].Split('_').Length < 4) { print("[AI] Peninsule : recette de comptoir du coin inutilisable (\"" + cc[42] + "\") -> recette par defaut pour porter le debord."); cc[42] = "1.25_0_0_0_1.75_0_0_0"; } // La projection va du COTE AVANT, donc sur le dos tourne vers l'exterieur du L : // BackRight pour un coin "R", BackLeft pour un "L" — la meme regle que sur le coin 2. // C'etait le champ 2 EN DUR, ce qui ne tombait juste que pour un coin "L". Tant que la // main n'etait pas reellement appliquee, tous les coins etaient des "L" et l'erreur // restait invisible ; elle est apparue des que le coin est sorti en "R". int c1AvantIndex = CornerAvantIndex(cornerFlip); cc[42] = DOIT.ChangeCodeOnPos(cc[42], c1AvantIndex, backProj.ToString(), "_"); DOIT.REDOCAB(cornerIdx); print("[AI] Peninsule : comptoir du coin 1 -> " + cc[42] + " (projection " + backProj + " sur " + (cornerFlip == "R" ? "BackRight" : "BackLeft") + ", champ " + c1AvantIndex + ", main " + cornerFlip + ")."); yield return _waitForSecondsRealtime2; } // 2) La RANGEE AVANT : caissons B1 tournés de 90°, dos à dos avec la rangée murale, qui prolongent // l'ALLÉE dans la pièce. Ils sont alignés sur le bord du coin côté allée — Delta[0] — et // non sur son bord opposé : la péninsule part dans le prolongement de la rangée. float baseDepth = _AI.BaseDepth + _AI.Door1Thickness; // Angle pris dans le sens forward -> mur, et non l'inverse : la face avant des caissons regarde // alors l'allée libre au lieu de la rangée murale. Pris dans l'autre sens ils sortaient dos // devant. float rowAngle = GetAngleFromXClockwise(Delta[0] + forward, Delta[0]); // L'ORIENTATION DOIT SUIVRE LA FACE EXPOSEE, pas seulement le mur. // Cet angle ne dependait que de 'forward' : il etait donc IDENTIQUE aux deux bouts du mur. // Or le panneau et la projection d'assise se posent en '-along', et 'along' change de sens // d'un bout a l'autre. A l'un des deux bouts, les portes se retrouvaient donc du cote du // panneau — d'ou des tabourets dans l'allee au lieu du COTE AVANT. // Set_TemporySC ecrit l'angle en rotation Y, donc transform.forward = (sin a, 0, cos a) ; // et AiGet.FowardbyObject rend ce meme vecteur, qui est la face EXPOSEE (c'est ce que la // correction du cote des tabourets a etabli). On exige donc qu'il pointe vers '-along'. float rowRad = rowAngle * Mathf.Deg2Rad; Vector3 rowBack = new(Mathf.Sin(rowRad), 0f, Mathf.Cos(rowRad)); if (Vector3.Dot(rowBack, -along) < 0f) { rowAngle = (rowAngle + 180f) % 360f; print("[AI] Peninsule : rangee avant retournee de 180 — sa face exposee regardait le " + "COTE INTERIEUR au lieu du COTE AVANT."); } Vector3 cursor = cabAnchor + along * (baseDepth * 0.5f) + forward * cornerDepth; float rowLen = 0f; foreach (var cab in rowCabs) rowLen += cab.w; float offset = 0f; foreach (var cab in rowCabs) { Vector3 p = cursor + forward * (offset + cab.w * 0.5f); p.y = _AI.BaseHeightPosition; // ADN[22] = "floor" et non le mur : ces caissons sont perpendiculaires et ne font pas // partie de la rangée murale. Les passes qui raisonnent en rangée — justification, // recentrage du lave-vaisselle, sens des poignées, dosseret — les ignorent alors // naturellement, au lieu de les décaler le long du mur. Set_TemporySC(cab.id, "floor", p, rowAngle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; // Sur "floor", CreateCAB prend le comptoir dans _G.SC[42] au lieu de la librairie. Non // renseigné, ADN[42] restait null et ConstructBASE plantait dès sa 15e ligne, sur // C[42].Split("_"). Même recette de comptoir que la rangée d'îlot : "H_gauche_arriere_ // droite_devant_...". La face AVANT des caissons regarde l'allée, donc l'ARRIÈRE est la // face ouverte : c'est là que le comptoir déborde pour asseoir les tabourets. // 12" avec tabourets, 1" sans — AVANT, un repli sur 10" laissait déborder le comptoir // de la péninsule même quand le design n'en demandait pas. _G.SC[42] = DOIT.ChangeCodeOnPos("1.25_0_10_0_1.75_0_0_0", 2, backProj.ToString(), "_"); _G.SC[45] = "OFF"; // cascade gauche _G.SC[47] = "OFF"; // cascade droite CreateCAB.AddCab(); // Mémorisé pour que CoroutineAddDishwasher puisse préférer la rangee avant de la péninsule à un // caisson de la rangée murale. if (_G.OBJnum > 0) _peninsulaRowCabs.Add(_G.OBJs[_G.OBJnum - 1][0]); offset += cab.w; yield return null; } // 3) RETOUR EN L ("Return") : un SECOND caisson de coin au bout de la rangee avant, puis une // rangée qui repart perpendiculairement. Sa longueur vient de la place disponible. if (IslandTypeIs("Return")) { yield return StartCoroutine(AddPeninsulaReturn(WallNumber, cabAnchor, along, forward, cornerDepth + rowLen, baseDepth, atWallStart)); // AVANT, un repli remettait ici le debord d'assise de 12" sur la rangee avant ET sur le // coin 1 quand aucun retour n'avait pu etre pose. Je l'avais ajoute de moi-meme, et il // contredit la regle : en "StoolR" l'assise est AU BOUT, le coin 1 ne porte qu'une // projection de 1". Il est retire. } // 4) PANNEAU décoratif sur la face ARRIÈRE — celle des tabourets, exposée dans la pièce. // Il court du MUR jusqu'au bout de la rangee avant, et pas seulement le long de la rangee avant : la joue // du caisson de coin est exposée elle aussi, s'arrêter a la rangee avant l'aurait laissée nue. // Même pièce que le panneau d'îlot (AddIslandPanel) : 0.5" d'épaisseur, 35.5" de haut. // La face arrière est à l'aplomb de cabAnchor ; le centre du meuble étant à une // demi-profondeur de là, le panneau se pose à -along * baseDepth/2 de ce centre. float panelLen = cornerDepth + rowLen; Vector3 panelCenter = cabAnchor + along * (baseDepth * 0.5f) + forward * (panelLen * 0.5f); AddIslandPanel(WallNumber, panelCenter, -along, panelLen, rowAngle, baseDepth * 0.5f); yield return _waitForSecondsRealtime2; print("[AI] Peninsule sur " + WallNumber + " : coin " + corner + " + " + rowCabs.Count + " caissons (rangee avant " + rowLen + "), debord de comptoir " + backProj + " sur la face ouverte, panneau arriere de " + panelLen + "."); yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ReflectionProbesUpdater.Instance.UpdateProbes(); _aiBusy--; yield return null; } // RETOUR EN L (_G.Island == "Return"). Au bout de la rangee avant de peninsule : un SECOND caisson de // coin, puis une rangee qui repart perpendiculairement, parallele au mur. // // Le COTE du virage n'est pas impose : on mesure les deux et on garde celui qui loge le plus // de caissons. A egalite c'est la face OUVERTE (-along, cote salle) qui gagne — virer vers la // rangee murale referme la cuisine en U et n'a de sens que si la salle ne laisse pas le choix. // // cabAnchor = origine du meuble sur le mur ; along = vers la rangee ; forward = vers la piece // tipV = avancee du bout de la rangee avant depuis cabAnchor, le long de forward // Longueurs de retour a essayer, de la plus grande a la plus petite. // Au-dela d'un caisson on empile des multiples de la largeur courante ; EN DECA on descend // dans les largeurs du catalogue. // AVANT, la boucle n'essayait que des multiples de baseWide (24") : des qu'un 24" sortait de // la zone, elle tombait directement a zero et le retour restait vide, alors qu'un 18" ou un 15" // y tenait. La regle est de reduire le caisson, et de ne se rabattre sur le coin seul que si // aucune largeur ne rentre. static List ReturnLengthCandidates(float baseWide) { // Largeurs REELLEMENT au catalogue, zero compris : zero est le retour vide, ou seul le coin 2 // reste, avec son panneau de bout. var widths = new List { 0f }; string[] ids = { _AI.Base36, _AI.Base33, _AI.Base30, _AI.Base27, _AI.Base24, _AI.Base21, _AI.Base18, _AI.Base15, _AI.Base12, _AI.Base9 }; foreach (string id in ids) { float w = SafeCabinetWidth(id); if (w > 0f && !widths.Contains(w)) widths.Add(w); } // COMBINAISONS : k caissons pleine largeur plus une largeur d'appoint. Avant, la liste ne // contenait que des multiples de baseWide puis les largeurs seules — un retour de 42" // (24 + 18) n'etait jamais essaye, et l'espace du COTE BOUT restait a moitie rempli. var lens = new List(); for (int k = 0; k < RETURN_MAX_CABS; k++) foreach (float w in widths) { float v = k * baseWide + w; if (v <= RETURN_MAX_CABS * baseWide + 0.01f && !lens.Contains(v)) lens.Add(v); } lens.Sort((a, b) => b.CompareTo(a)); // de la plus grande a la plus petite return lens; } // Longueur de retour que la ZONE DISPONIBLE autorise, pour un cote et un modele de coin donnes. // On sonde la carte d'occupation depuis le bout du coin jusqu'au premier point occupe. Si le // sondage atteint le MUR sans rien rencontrer, on reserve en plus une profondeur de rangee // murale : le bord de la zone est a 48" de cette RANGEE, pas du mur nu, et la zone ne se // decoupe pas couloir par couloir. Enfin on retranche l'allee et la projection de comptoir de la // joue de bout, qui doit rester dans la zone elle aussi. // // Cette borne est calculee PAR MODELE DE COIN, et non apres coup : sinon un coin 90 pouvait // etre retenu sur une longueur de 48" que la zone n'autorise pas, puis se retrouver avec un // retour ramene a presque rien — alors que la regle veut un coin MORT des que le retour est court. static float ReturnZoneLimit(bool[,] occ, int cols, int rows, float ox, float oz, Vector3 cabAnchor, Vector3 forward, Vector3 returnDir, float tipV, PeninsulaCorner pc, float baseDepth, float shift) { float returnStart = shift + pc.Wide; float returnV = tipV + pc.Depth - baseDepth * 0.5f; float freeRun = 0f; Vector3 stopAt = cabAnchor + forward * returnV + returnDir * returnStart; while (freeRun < 240f) { Vector3 probe = cabAnchor + forward * returnV + returnDir * (returnStart + freeRun + ISLAND_GRID_STEP * 0.5f); if (!RectFree(occ, cols, rows, ox, oz, probe, forward, baseDepth, returnDir, ISLAND_GRID_STEP, out Vector3 hit)) { stopAt = hit; break; } freeRun += ISLAND_GRID_STEP; } // Le sondage s'arrete au bord de la ZONE : l'allee de 48" et la profondeur de la rangee // murale en ont deja ete retirees a la construction de la carte. Il ne reste donc a // retrancher que le prolongement de comptoir de la joue de bout, qui doit tenir dedans. // AVANT, on retirait ici 48" et une profondeur de rangee que la carte avait deja enlevees : // le retour se retrouvait plafonne a presque rien. return freeRun - NoStoolOverhang; } IEnumerator AddPeninsulaReturn(string WallNumber, Vector3 cabAnchor, Vector3 along, Vector3 forward, float tipV, float baseDepth, bool atWallStart) { float baseWide = SafeCabinetWidth(_AI.BASE); if (baseWide <= 0f) baseWide = 24f; // DEBORD DE COMPTOIR du retour : 12 pouces SEULEMENT si l'assise est au bout ("StoolR"), // 1 sinon. Une assise le long de l'allee ne justifie aucun debord ici. float returnProj = ReturnSeatProjection(); // Debord de la RANGEE AVANT. C'est LUI que le coin 2 et le retour doivent contourner, pas le leur : // les deux valeurs different des que l'assise n'est que d'un cote. float rowProj = RowSeatProjection(); // ZONE DISPONIBLE, reconstruite maintenant : elle contient donc le caisson de coin et la // rangee avant qui viennent d'etre poses, et le retour du L ne peut pas les recouvrir. // C'est la MEME carte que celle de la recherche : les bandes de 48"/72" y sont deja, il n'y // a plus d'allee a tester separement. BuildPeninsulaZone(out bool[,] occ, out int cols, out int rows, out float ox, out float oz); PeninsulaCorner bestPc = null; Vector3 bestDir = Vector3.zero; float bestLen = 0f; // Espace du COTE BOUT reellement occupe par la combinaison retenue : ecart du au debord du // rangee avant + largeur du coin + retour + projection de comptoir de la joue de bout. // C'est LUI qu'on maximise, et non la seule longueur du retour : comparer les pieds faisait // preferer un coin etroit suivi d'un caisson large a un coin large qui remplit mieux // l'ensemble. Le choix du modele de coin decoule donc du total, comme il se doit. float bestFilled = -1f; // Memes regles que pour la rangee avant : 48" d'allee au bout du retour, et une longueur qui suit // la place au lieu d'etre plafonnee. // Les degagements — passage au bout du retour, face de travail devant le coin 2 — sont // maintenant PORTES PAR LA ZONE, bande par mur. Les tester ici en plus les compterait deux // fois : un caisson devrait alors degager 48" a l'interieur d'une carte qui a deja retire // ces memes 48". // Il ne reste a reserver que le PROLONGEMENT DE COMPTOIR de la joue de bout : la zone // raisonne en carcasses, et ce debord doit tenir dedans lui aussi. const float returnAisleTest = NoStoolOverhang; // -along d'abord : a egalite de longueur, c'est lui qui est retenu. Vector3[] dirs = { -along, along }; foreach (Vector3 returnDir in dirs) { // ECART DU AU DEBORD DU RANGEE AVANT. Le comptoir de la rangee avant sort de returnProj sur sa face // exposee, celle de '-along' — la meme que couvre le panneau arriere. Le coin 2 et le // retour se posaient a ras de la FACE DES CAISSONS, donc SOUS ce debord : les caissons // doivent s'ecarter d'autant pour lui laisser sa place. Leur face vient alors affleurer // le bord du comptoir, et les deux debords se rejoignent en une seule surface. // Cote '+along' la rangee avant ne deborde pas (index 3 de la recette = 0) : aucun ecart. float dirShift = Vector3.Dot(returnDir, -along) > 0.5f ? rowProj : 0f; foreach (PeninsulaCorner pc in PeninsulaCornerModels(atWallStart, baseDepth, true)) { // Ce que la ZONE autorise pour CE modele de coin : c'est elle qui doit ecarter un // coin 90 trop gourmand, avant meme les tests d'encombrement. float zoneMax = ReturnZoneLimit(occ, cols, rows, ox, oz, cabAnchor, forward, returnDir, tipV, pc, baseDepth, dirShift); print("[AI] Peninsule en L : zone vers " + returnDir.x.ToString("0.0") + "," + returnDir.z.ToString("0.0") + " coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + (pc.Turned ? " EN TRAVERS" : "") + " (" + pc.Wide + "x" + pc.Depth + ") -> retour au plus " + zoneMax + "."); // La derniere candidate vaut 0 : le second coin SEUL, sans retour. Vos schemas // posent les deux coins pour borner l'espace, puis remplissent — le coin ne doit // donc pas disparaitre quand plus aucune largeur ne rentre. Sa joue de bout recoit // alors le panneau, comme celle du dernier caisson d'un retour. foreach (float len in ReturnLengthCandidates(baseWide)) { if (len > zoneMax + 0.01f) continue; // hors de la zone disponible // Meme regle qu'au premier coin : un coin 90 exige que le retour pose sur son // cote fasse au moins CORNER_L_MIN_RUN. En dessous -> coin mort. if (!pc.Dead && len < CORNER_L_MIN_RUN) { print("[AI] Peninsule en L : coin L " + pc.Id + " ecarte, retour de " + len + " < " + CORNER_L_MIN_RUN + " de degagement -> coin mort a la place."); continue; } // Le coin s'appuie sur le bout de la rangee avant et deborde de sa profondeur vers returnDir. Vector3 cornerCenter = cabAnchor + forward * (tipV + pc.Depth * 0.5f) + returnDir * (dirShift + pc.Wide * 0.5f); bool cornerFree = RectFree(occ, cols, rows, ox, oz, cornerCenter, forward, pc.Depth, returnDir, pc.Wide, out Vector3 hitC); // Le retour : meme profondeur qu'un caisson de base, mesuree sur forward, et // FLUSH avec la face ELOIGNEE du coin. Le L s'enroule : en descendant la rangee avant // la face exterieure est celle de l'ancre, apres le virage c'est l'autre bord. // Aligner sur la face proche laissait une marche de (pc.Depth - baseDepth). float returnV = tipV + pc.Depth - baseDepth * 0.5f; Vector3 hitRet = Vector3.zero; Vector3 returnCenter = cabAnchor + forward * returnV + returnDir * (dirShift + pc.Wide + len * 0.5f); bool returnFree = len <= 0f || RectFree(occ, cols, rows, ox, oz, returnCenter, forward, baseDepth, returnDir, len, out hitRet); // Allee au bout du retour, sur la meme bande que lui — elargie du debord, // qui court jusqu'au bout du dernier caisson. Vector3 aisleCenter = cabAnchor + forward * (returnV + returnProj * 0.5f) + returnDir * (dirShift + pc.Wide + len + returnAisleTest * 0.5f); bool aisleFree = RectFree(occ, cols, rows, ox, oz, aisleCenter, forward, baseDepth + returnProj, returnDir, returnAisleTest, out Vector3 hitA); // DEBORD DU COMPTOIR sur la face exposee du coin 2 ET du retour — la face // '+forward', celle que couvre le panneau du retour. Le comptoir sort de returnProj // au-dela des caissons : cette bande doit etre libre, sinon on repose le // probleme deja corrige sur la rangee avant, ou un caisson de 12 pouces se // retrouvait superpose a la projection. // Declare AVANT le || : le court-circuit laisserait hitP non assigne (CS0165). Vector3 hitP = Vector3.zero; bool projFree = returnProj <= 0f || RectFree(occ, cols, rows, ox, oz, cabAnchor + forward * (tipV + pc.Depth + returnProj * 0.5f) + returnDir * (dirShift + (pc.Wide + len) * 0.5f), forward, returnProj, returnDir, pc.Wide + len, out hitP); // L'allee devant le second coin n'est plus testee ici : la ZONE la porte. if (!cornerFree || !returnFree || !aisleFree || !projFree) { print("[AI] Peninsule en L : retour de " + len + " refuse vers " + returnDir.x.ToString("0.0") + "," + returnDir.z.ToString("0.0") + " coin " + pc.Id + " - " + (cornerFree ? "" : "coin hors zone ou bloque par " + BlockerAt(hitC) + " ; ") + (returnFree ? "" : "retour hors zone ou bloque par " + BlockerAt(hitRet) + " ; ") + (aisleFree ? "" : "joue de bout hors zone : " + BlockerAt(hitA) + " ; ") + (projFree ? "" : "debord de comptoir (" + returnProj + ") hors zone : " + BlockerAt(hitP) + " ; ")); continue; } float filled = dirShift + pc.Wide + len + NoStoolOverhang; // A VALEUR EGALE, un coin 90 l'emporte sur un coin mort : son rangement est // accessible la ou celui du coin mort est perdu. L'ordre de la liste le donnait // deja a l'interieur d'une direction, mais implicitement — et pas du tout entre // les deux directions d'essai, ou la comparaison repart du meilleur connu. bool tie = Mathf.Abs(filled - bestFilled) <= 0.01f; bool better = bestPc == null || filled > bestFilled + 0.01f || (tie && bestPc.Dead && !pc.Dead); if (better) { bestFilled = filled; bestLen = len; bestDir = returnDir; bestPc = pc; print("[AI] Peninsule en L candidat vers " + returnDir.x.ToString("0.0") + "," + returnDir.z.ToString("0.0") + " : coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + (pc.Turned ? " EN TRAVERS" : "") + " (" + pc.Wide + "x" + pc.Depth + ") + un retour de " + len + " -> cote Bout rempli sur " + filled + "."); } else { print("[AI] Peninsule en L : coin " + (pc.Dead ? "MORT " : "L ") + pc.Id + (pc.Turned ? " EN TRAVERS" : "") + " + retour de " + len + " remplit " + filled + (tie ? " a egalite avec " : " < ") + bestFilled + " -> ecarte" + (tie ? " (priorite au coin 90 deja retenu)" : "") + "."); } break; } } } if (bestPc == null) { print("[AI] Peninsule en L : aucun cote ne loge un retour, meme d'un seul caisson -> peninsule droite."); yield break; } print("[AI] Peninsule en L : retour retenu de " + bestLen + " (degagements portes par la ZONE ; il ne reste ici que " + returnAisleTest + " de debord de comptoir sur la joue de bout)."); // --- Pose : second caisson de coin, puis le retour. // Main OPPOSEE au premier coin : le L se referme. Le premier ayant pu etre determine // par l'objet le plus proche, on repart de la valeur reellement posee. string cornerFlip = _peninsulaFirstCornerHand == "L" ? "R" : "L"; // ANGLE : ce coin termine la RANGEE AVANT, perpendiculaire au mur — pas le mur. Lui laisser // l'angle du mur le laissait oriente comme le premier coin, sans tourner le virage. // Rotation demandee : -90 de plus que la precedente, soit -180 depuis l'angle du mur. float wallAngle = GetAngleByWall(WallNumber); const float returnTurn = -180f; float cornerAngle = wallAngle + returnTurn + (bestPc.Turned ? DeadCornerTurn : 0f); // Meme ecart que celui mesure, recalcule sur la direction retenue : tout ce qui suit // le coin 2 part de returnShift et non de zero. float returnShift = Vector3.Dot(bestDir, -along) > 0.5f ? rowProj : 0f; // ---- BORD DE LA ZONE DISPONIBLE, mesure directement sur la carte d'occupation. // Les bandes RectFree de la boucle VALIDENT une longueur donnee ; elles ne disent pas ou // se trouve reellement l'obstacle. Ici on avance pas a pas depuis le depart du retour // jusqu'au premier point occupe, puis on borne la longueur des caissons pour que le // COMPTOIR — carcasse plus sa projection de joue — s'arrete a RETURN_CLEAR de cet obstacle. // C'est la regle de la zone : caissons et prolongements de comptoir a l'interieur, seuls // les tabourets debordent. { float returnStart = returnShift + bestPc.Wide; float returnVzone = tipV + bestPc.Depth - baseDepth * 0.5f; float freeRun = 0f; Vector3 stopAt = cabAnchor + forward * returnVzone + bestDir * returnStart; while (freeRun < 240f) { Vector3 probe = cabAnchor + forward * returnVzone + bestDir * (returnStart + freeRun + ISLAND_GRID_STEP * 0.5f); if (!RectFree(occ, cols, rows, ox, oz, probe, forward, baseDepth, bestDir, ISLAND_GRID_STEP, out Vector3 hitZone)) { stopAt = hitZone; break; } freeRun += ISLAND_GRID_STEP; } // Le sondage s'arrete AU BORD DE LA ZONE : l'allee de 48" et la profondeur de la // rangee murale en ont deja ete retirees a la construction de la carte. Il ne reste // donc a retrancher que le prolongement de comptoir de la joue de bout. // AVANT, on retirait ici une seconde fois 48" et une profondeur de rangee : le retour // se retrouvait plafonne a presque rien. float maxLeg = freeRun - NoStoolOverhang; print("[AI] Peninsule en L : zone disponible — " + freeRun + " libres depuis le depart du retour jusqu'au bord de la zone (arret sur " + BlockerAt(stopAt) + "), moins " + NoStoolOverhang + " de debord -> retour au plus " + maxLeg + " (retenu " + bestLen + ")."); if (bestLen > maxLeg) { bestLen = Mathf.Max(0f, maxLeg); print("[AI] Peninsule en L : retour ramene a " + bestLen + " pour rester dans la zone disponible."); if (!bestPc.Dead && bestLen < CORNER_L_MIN_RUN) print("[AI] Peninsule en L : attention, le coin " + bestPc.Id + " est un coin 90 et son retour tombe sous " + CORNER_L_MIN_RUN + " — le modele avait ete choisi sur la longueur d'avant la borne."); } } // TRACE DE POSITIONNEMENT. "Mal positionne" peut vouloir dire quatre choses selon l'axe // en cause ; ces reperes les separent sans avoir a deviner. Tout est en pouces monde. // ancre = face exposee des caissons de la rangee avant (plan de son panneau arriere) // bord compt. = bord du debord de la rangee avant, returnProj plus loin sur -along // coin2 proche = face du coin 2 qui regarde la rangee avant, apres l'ecart // coin2 loin = sa face opposee, la ou commence le retour Vector3 dbgAnchor = cabAnchor + forward * (tipV + bestPc.Depth * 0.5f); Vector3 dbgEdge = dbgAnchor + (-along) * rowProj; Vector3 dbgNear = dbgAnchor + bestDir * returnShift; Vector3 dbgFar = dbgAnchor + bestDir * (returnShift + bestPc.Wide); print("[AI] Peninsule en L reperes : along=" + along.x.ToString("0.00") + "," + along.z.ToString("0.00") + " forward=" + forward.x.ToString("0.00") + "," + forward.z.ToString("0.00") + " bestDir=" + bestDir.x.ToString("0.00") + "," + bestDir.z.ToString("0.00") + " | debord=" + returnProj + " ecart=" + returnShift + " | ancre=" + dbgAnchor.x.ToString("0.0") + "," + dbgAnchor.z.ToString("0.0") + " bord compt.=" + dbgEdge.x.ToString("0.0") + "," + dbgEdge.z.ToString("0.0") + " coin2 proche=" + dbgNear.x.ToString("0.0") + "," + dbgNear.z.ToString("0.0") + " coin2 loin=" + dbgFar.x.ToString("0.0") + "," + dbgFar.z.ToString("0.0") + " | tipV=" + tipV + " profCoin=" + bestPc.Depth + " largCoin=" + bestPc.Wide + " profBase=" + baseDepth + "."); Vector3 pos = cabAnchor + forward * (tipV + bestPc.Depth * 0.5f) + bestDir * (returnShift + bestPc.Wide * 0.5f); pos.y = _AI.BaseHeightPosition; print("[AI] Peninsule en L : second coin " + bestPc.Id + " main " + cornerFlip + ", angle mur " + wallAngle + " + " + returnTurn + " = " + cornerAngle + ", ecart " + returnShift + " pour le debord de la rangee avant."); Set_TemporySC(bestPc.Id, "floor", pos, cornerAngle, CornerBaseHand(bestPc)); _G.SC[23] = CornerBaseHand(bestPc); // meme main de depart qu'au premier coin if (bestPc.Dead) _G.SC[4] = "B10"; // ligne de base ; la main vient de FlipCornerTo _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; _G.SC[56] = "0,0,0,0"; // Comptoir du caisson de coin : valeur donnee, la meme pour une main "L" et une "R". // Le debord sur les cotes exposes (CP[2] et CP[3]) et 1.75" devant, la ou est la porte. // La disposition d'un caisson de coin n'est pas celle d'un caisson droit : ni les index de // "H_gauche_arriere_droite_devant", ni le CP[1]=gauche / CP[4]=droite que j'avais deduit // de ConstructCORNERBASE ne s'appliquent ici. // Les deux cotes exposes portent returnProj et non 1 en dur : ce sont eux qui prolongent le // debord de la rangee avant d'un cote et celui du retour de l'autre, les trois plans doivent se // rejoindre. Sans tabourets returnProj vaut 1 et la recette reste mot pour mot celle validee. // Les deux DOS du coin 2 ne portent PAS la meme valeur : chacun prolonge la branche du // L qu'il longe. // dos du COTE AVANT -> debord de la RANGEE AVANT (rowProj) : 12" en "Stool", 1" en "StoolR" // dos du COTE BOUT -> debord du RETOUR (returnProj) : 1" en "Stool", 12" en "StoolR" // Lequel est lequel depend de la main : coin "R" -> Avant = BackRight, Bout = BackLeft ; // coin "L" -> l'inverse. // AVANT, la recette posait returnProj sur les DEUX dos avant meme la correction de main : en // "StoolR" les 12" du retour se retrouvaient donc aussi du COTE AVANT. // Sans tabourets les deux valent 1 et la recette reste celle que vous aviez validee. int c2AvantIndex = CornerAvantIndex(cornerFlip); int c2BoutIndex = CornerAvantIndex(cornerFlip == "R" ? "L" : "R"); // le dos oppose // Face vers le COTE INTERIEUR : c'est le BOUT de l'arme qui porte la face du COTE BOUT. // Les quatre cotes se suivent — Left, BackLeft, BackRight, Right — et le bout d'une arme // est voisin de sa face exterieure. Coin "R" : le Bout est BackLeft, donc l'interieur est // Left ; coin "L" : l'inverse. // Elle restait a 0 : le retour la couvre quand il existe, mais quand il est vide c'est // elle qui porte le panneau de bout, et le comptoir s'arretait a ras du caisson. int c2InterieurIndex = cornerFlip == "R" ? CornerLeftIndex : CornerRightIndex; string c2Counter = "1.25_0_0_0_0_1.75_1.75_0"; c2Counter = DOIT.ChangeCodeOnPos(c2Counter, c2AvantIndex, rowProj.ToString(), "_"); c2Counter = DOIT.ChangeCodeOnPos(c2Counter, c2BoutIndex, returnProj.ToString(), "_"); c2Counter = DOIT.ChangeCodeOnPos(c2Counter, c2InterieurIndex, NoStoolOverhang.ToString(), "_"); _G.SC[42] = c2Counter; print("[AI] coin 2 : comptoir " + c2Counter + " (main " + cornerFlip + " -> Avant=" + (cornerFlip == "R" ? "BackRight" : "BackLeft") + " champ " + c2AvantIndex + " = " + rowProj + " ; Bout=" + (cornerFlip == "R" ? "BackLeft" : "BackRight") + " champ " + c2BoutIndex + " = " + returnProj + " ; Interieur=" + (cornerFlip == "R" ? "Left" : "Right") + " champ " + c2InterieurIndex + " = " + NoStoolOverhang + ")."); CreateCAB.AddCab(); int corner2Idx = _G.OBJnum - 1; if (corner2Idx >= 0) _peninsulaCornerCabs.Add(_G.OBJs[corner2Idx][0]); yield return _waitForSecondsRealtime2; yield return FlipCornerTo(corner2Idx, cornerFlip, "coin 2"); // PANNEAU sur le cote expose du coin 2 — _AI.PANEL_ISLAND, la meme piece que le panneau // arriere de la peninsule, au lieu du recouvrement C[81] : Recover.ConstructPanel ne sait // placer un recouvrement de coin qu'en position 0 ou 3, et meme la le resultat ne sortait // pas. Un panneau pose comme les autres est previsible. // Il prolonge exactement le panneau arriere de la peninsule : meme plan (cote de l'ancre), // sur la profondeur du coin, la ou l'autre s'arretait au bout de la rangee avant. float c2PanelAngle = GetAngleFromXClockwise(cabAnchor + forward, cabAnchor); Vector3 c2PanelCenter = cabAnchor + forward * (tipV + bestPc.Depth * 0.5f) + bestDir * returnShift; AddIslandPanel(WallNumber, c2PanelCenter, forward, bestPc.Depth, c2PanelAngle, 0f); print("[AI] coin 2 : panneau " + _AI.PANEL_ISLAND + " sur le cote, longueur " + bestPc.Depth + "."); yield return _waitForSecondsRealtime2; // Largeurs variees, comme la rangee avant et la rangee d'ilot. Pas d'evier ici : il est deja // place, au mur ou dans la rangee avant. var returnCabs = BuildIslandCabinets(bestLen, false); // Angle pris comme celui de la rangee avant, mais sur l'axe du retour : la face avant regarde alors // le meme cote que celle de la rangee avant. float returnAngle = GetAngleFromXClockwise(cabAnchor + bestDir, cabAnchor); Vector3 cursor = cabAnchor + forward * (tipV + bestPc.Depth - baseDepth * 0.5f) + bestDir * (returnShift + bestPc.Wide); float offset = 0f; for (int li = 0; li < returnCabs.Count; li++) { var cab = returnCabs[li]; Vector3 p = cursor + bestDir * (offset + cab.w * 0.5f); p.y = _AI.BaseHeightPosition; Set_TemporySC(cab.id, "floor", p, returnAngle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; // Projection ARRIERE = le debord de la peninsule, index 2 de // "H_gauche_arriere_droite_devant_...". La face arriere du retour est sa face exposee, // celle que couvre le panneau : c'est la que s'asseoient les tabourets, comme sur le // rangee avant. Figee a 1, le retour du L n'en portait pas. string returnCounter = DOIT.ChangeCodeOnPos("1.25_0_10_0_1.75_0_0_0", 2, returnProj.ToString(), "_"); // DERNIER caisson du retour : sa joue est habillee d'un panneau, le comptoir doit donc // le recouvrir de 1". Cote GAUCHE (index 1) : c'est la convention de PlaceCabRun pour // la rangee d'ilot, ou la projection gauche revient au dernier caisson d'une rangee // posee dans l'ordre (leftFall = i == count - 1). Le retour est pose de la meme facon, // dans l'ordre de bestDir. if (li == returnCabs.Count - 1) returnCounter = DOIT.ChangeCodeOnPos(returnCounter, 1, NoStoolOverhang.ToString(), "_"); _G.SC[42] = returnCounter; _G.SC[45] = "OFF"; _G.SC[47] = "OFF"; CreateCAB.AddCab(); if (_G.OBJnum > 0) { // Dans les deux listes : le lave-vaisselle lit _peninsulaRowCabs, les tabourets // "StoolR" ont besoin du retour seul. _peninsulaRowCabs.Add(_G.OBJs[_G.OBJnum - 1][0]); _peninsulaReturnCabs.Add(_G.OBJs[_G.OBJnum - 1][0]); } offset += cab.w; yield return null; } float posed = 0f; foreach (var cab in returnCabs) posed += cab.w; // Le debord d'assise du retour doit regarder la SECTION DISPONIBLE, cernee par le L : le // cote '-forward', entre le mur et le retour. Selon l'orientation du caisson, c'est // l'ARRIERE (champ 2) ou le DEVANT (champ 4) de la recette qui donne de ce cote. // On le lit sur l'objet POSE plutot que de le deduire de l'angle : FowardbyObject rend la // direction des PORTES — c'est ce qu'a etabli la correction du cote des tabourets — donc // la face de debord est l'opposee. // Ne touche que "StoolR" : en "Stool" returnProj vaut 1 et il n'y a pas d'assise ici. if (returnProj > NoStoolOverhang) { foreach (string n in _peninsulaReturnCabs) { GameObject go = Get.o2("SCENE", n); if (go == null) continue; bool rearFacesSection = Vector3.Dot(-AiGet.FowardbyObject(go).normalized, -forward) > 0f; if (rearFacesSection) continue; // deja au bon endroit, champ 2 for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i) || _G.OBJs[i][0] != n) continue; string[] r = _G.OBJs[i]; if (string.IsNullOrEmpty(r[42]) || r[42].Split('_').Length < 5) break; r[42] = DOIT.ChangeCodeOnPos(r[42], 2, NoStoolOverhang.ToString(), "_"); r[42] = DOIT.ChangeCodeOnPos(r[42], 4, returnProj.ToString(), "_"); DOIT.REDOCAB(i); print("[AI] retour du L : debord d'assise deplace sur le DEVANT (champ 4) pour " + n + " -> " + r[42] + "."); break; } } yield return _waitForSecondsRealtime2; } // PANNEAU sur l'arriere visible du coin 2 ET du retour. Le panneau de la peninsule droite // s'arrete au bout de la rangee avant ; celui-ci prend le relais sur l'autre branche du L. // Il court sur (largeur du coin + longueur du retour) et se pose sur la face ELOIGNEE du // retour — celle qui donne sur la piece, l'autre etant contre l'allee de travail. float returnPanelLen = bestPc.Wide + posed; Vector3 returnPanelCenter = cabAnchor + forward * (tipV + bestPc.Depth - baseDepth * 0.5f) + bestDir * (returnShift + returnPanelLen * 0.5f); AddIslandPanel(WallNumber, returnPanelCenter, forward, returnPanelLen, returnAngle, baseDepth * 0.5f); yield return _waitForSecondsRealtime2; print("[AI] Peninsule en L : panneau arriere du retour de " + returnPanelLen + " (coin " + bestPc.Wide + " + retour " + posed + ")."); // PANNEAU sur le BOUT du retour : la joue du dernier caisson est exposee, rien ne vient // s'appuyer dessus. Meme piece que les autres ; sa longueur est la profondeur d'un caisson // et elle court le long de 'forward', d'ou le meme angle que le panneau du coin 2. // Sans retour, cette meme joue est celle du COIN 2 : elle reste apparente et doit porter // le panneau elle aussi. Le calcul est le meme, 'posed' valant alors 0. Vector3 endPanelCenter = cabAnchor + forward * (tipV + bestPc.Depth - baseDepth * 0.5f) + bestDir * (returnShift + bestPc.Wide + posed); AddIslandPanel(WallNumber, endPanelCenter, forward, baseDepth, c2PanelAngle, 0f); print("[AI] Peninsule en L : panneau " + _AI.PANEL_ISLAND + " sur la joue apparente " + (posed > 0f ? "du dernier caisson du retour." : "du coin 2 (aucun retour).")); yield return _waitForSecondsRealtime2; print("[AI] Peninsule en L pose : coin " + bestPc.Id + " + " + returnCabs.Count + " caissons (" + posed + ") vers " + bestDir.x.ToString("0.0") + "," + bestDir.z.ToString("0.0") + ", debord de comptoir " + returnProj + " sur la face exposee."); yield return null; } // Amene le caisson 'idx' a la main 'wanted' en passant par CONTROL.FlipCabinet — la meme // transformation que le bouton FLIP, deja validee. Ne fait rien si la main est deja bonne. // Panneau de RECOUVREMENT sur le cote expose d'un caisson de coin. // // C'est C[81] qui porte les recouvrements, et lui seul : // - C[78] est la SORTE de panneau (Std / Over / Notch / FGN) — pas a toucher ici ; // - C[83] est le FINI des panneaux — pas a toucher sur un caisson de coin. // AddRecoverToCabinet lit C[81] en entrees separees par des VIRGULES, une par cote — quatre // pour un coin (B2_B4_B9_B6_B11...) — et pose un panneau des que l'entree vaut autre chose // que "0". D'ou les "0,0,0,0" des fichiers sauvegardes : aucun recouvrement. // Cote habille selon la main : "R" -> Right (3), "L" -> Left (0). // A appeler APRES FlipCornerTo, qui echange les cotes du caisson. // Main de DEPART d'un caisson de coin, avant toute inversion : // coin MORT -> "R", pose avec le modele B10, comme AddBASECorners("Dead_R") ; // coin 90 -> "L", la valeur du catalogue — les deux references y sont de base "L". // FlipCornerTo part de cette valeur connue et inverse par CONTROL.FlipCabinet si besoin. static string CornerBaseHand(PeninsulaCorner pc) => pc != null && pc.Dead ? "R" : "L"; IEnumerator FlipCornerTo(int idx, string wanted, string label) { if (idx < 0 || _G.OBJs[idx] == null || _G.OBJs[idx][0] == "null") yield break; // La main REELLE d'un caisson de coin est portee par son CODE MODELE, pas par ADN[23] : // ConstructCORNERBASE ne lit jamais ADN[23], et AddBASECorners("Dead_R") pose le couple // "R" + B10. Donc B10 = geometrie DROITE, B9 = geometrie GAUCHE. // AVANT je comparais ADN[23] a 'wanted' — or je venais d'ecrire moi-meme ADN[23] = wanted // juste avant la creation : les deux etaient toujours egaux, l'inversion ne partait jamais, // et le journal annoncait "main deja L" sur un caisson pourtant en B10. // COIN MORT : la main est portee par le CODE MODELE (B10 = droite, B9 = gauche). // COIN 90 : elle ne l'est pas — le modele est le meme des deux cotes et la geometrie vient // du catalogue, ou les deux references sont de base "L". // AVANT, tout modele hors B9/B10 sortait d'ici sans rien faire : un coin 90 gardait donc // la main du catalogue quoi qu'on ait calcule, et ressortait toujours "L". string model = _G.OBJs[idx][4]; string current = (model == "B9" || model == "B10") ? (model == "B10" ? "R" : "L") : "L"; if (current == wanted) { print("[AI] " + label + " : main deja " + wanted + " (modele " + model + ")."); yield break; } CONTROL.FlipCabinet(idx); DOIT.REDOCAB(idx); print("[AI] " + label + " : main " + current + " -> " + _G.OBJs[idx][23] + " par FlipCabinet (modele " + _G.OBJs[idx][4] + ")."); yield return _waitForSecondsRealtime2; } // CornerModel : caisson de coin retenu par ChooseCornerBaseModel (Model == "90"). Vide -> _AI.CB90. IEnumerator AddBASECorners(string Model, string WallNumber, List Delta, float SpaceWall_1, string CornerModel = null) { // Section du coin (jonction = Delta[0]) désactivée -> pas de caisson de coin ici. if (!SectionAllowed(WallNumber, Delta[0])) { print("[AI] coin " + WallNumber + " : section de l'angle non autorisée -> caisson de coin abandonné."); yield break; } print("Wall.name=======" + WallNumber + " Model====" + Model); Vector3 direction = (Delta[1] - Delta[0]).normalized; Vector3 PositionToInstall; Vector3 Forward = -Get.o1(WallNumber).transform.forward.normalized; float angle = GetAngleFromXClockwise(Delta[0], Delta[1]); if (Model == "90") { string corner = string.IsNullOrEmpty(CornerModel) ? _AI.CB90 : CornerModel; // ValueFloat : même raison que dans ChooseCornerBaseModel (profondeur notée "a_b"). float Depth = UIT.ValueFloat(Library.Cabinet, corner, Header.C); PositionToInstall = Delta[0] + direction.normalized * Depth * 0.5f; PositionToInstall += Forward * Depth * 0.5f; PositionToInstall.y = _AI.BaseHeightPosition; Set_TemporySC(corner, WallNumber, PositionToInstall, angle, "F"); } if (Model == "Dead_R") { angle = GetAngleFromXClockwise(Delta[1], Delta[0]); // Bord du caisson collé au coin (fond du mur) = demi-largeur réelle depuis Delta[0]. float deadW = UIT.ValueFloat(Library.Cabinet, _AI.CBDEADR, Header.A); if (deadW <= 0f) deadW = 48f; PositionToInstall = Delta[0] + direction.normalized * (deadW * 0.5f); PositionToInstall += 0.5f * (_AI.BaseDepth+_AI.Door1Thickness) * Forward; PositionToInstall.y = _AI.BaseHeightPosition; // "R" en dur : la regle de l'objet le plus proche (CornerHandFromNearest) ne vaut // QUE pour le premier coin d'une peninsule ou d'un Return, pas pour les coins de mur. Set_TemporySC(_AI.CBDEADR, WallNumber, PositionToInstall, angle, "R"); _G.SC[23] = "R"; _G.SC[4] = "B10"; } if (Model == "Dead_L") { //float wide = Vector3.Distance(Delta[0], Delta[1]); if (SpaceWall_1 > 51) {SpaceWall_1 = 51;} PositionToInstall = Delta[0] + 0.5f * SpaceWall_1 * direction.normalized; PositionToInstall += 0.5f * _AI.BaseDepth * Forward; PositionToInstall.y = _AI.BaseHeightPosition; Set_TemporySC(_AI.CBDEADL, WallNumber, PositionToInstall, angle, "L"); _G.SC[6]= SpaceWall_1.ToString(); } _G.SC[55] = "0,0,0,0"; _G.SC[56] = "0,0,0,0"; _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[28] = _AI.BaseDepth.ToSafeString(); _G.SC[29] = _AI.BaseDepth.ToSafeString(); CreateCAB.AddCab(); yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); ReflectionProbesUpdater.Instance.UpdateProbes(); yield return null; } //-------------------------------SET SINK CABINET------------------------------------------------------------------ public void SetSinkCabinet() { //Get side space GameObject Target = AiGet.SinkOnWall(WallSelected); //if spcae < 9 reduce sink cabinet //_AI.BASESINK = "SB33"; ChangeCabinet(Target, _AI.BASESINK); } //-------------------------------CORNER WALL CABINET------------------------------------------------------------------ public void AddCornerWallCabinet() => StartCoroutine(AddCornerWallCabinetCo()); private IEnumerator AddCornerWallCabinetCo() { if (WallSelected.Count > 1) { //GameObject Wall; string PreviousWallName; Vector3 StartPoint; Vector3 EndPoint; List Delta; foreach (string Wallstring in WallSelected) { //Wall = Get.o2("ROOM", Wallstring.Replace("m", "w")); PreviousWallName = AiGet.PreviousWallName(Wallstring); // Plage hauteur = celle des caissons muraux : capte les fenêtres, sinon SpaceOnWall // ignorait la fenêtre proche du coin et posait un coin dessus (section 1 trop courte). List WallObstacles = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.CeilingLine, _AI.FloorLine + 64); List PreviousObstacles = AiGet.AllObjectsOnWall(PreviousWallName, "All", "All", _AI.CeilingLine, _AI.FloorLine + 64); float AngleBetween = AiGet.AngleBetweenWallAndPrevious(Wallstring, PreviousWallName); // Min avec CornerSolidSpace (modèle de sections) : capte les fenêtres que la détection // par hauteur d'AllObjectsOnWall peut rater -> pas de coin mural sur une fenêtre proche. float SpaceOnWall = Mathf.Min( AiGet.WallSpaceDeltaOnWallFrom(Wallstring, WallObstacles, "From Start"), CornerSolidSpace(Wallstring, false)); float SpaceOnPrevious = Mathf.Min( AiGet.WallSpaceDeltaOnWallFrom(PreviousWallName, PreviousObstacles, "From End"), CornerSolidSpace(PreviousWallName, true)); // Espace requis pour un vrai coin mural = dimension (le long du mur) du caisson CW90. float cw90 = string.IsNullOrEmpty(_AI.CW90) ? 0f : UIT.ValueFloat(Library.Cabinet, _AI.CW90, Header.A); if (WallSelected.Contains(PreviousWallName.Replace("w", "m")) && AngleBetween < 91 && AngleBetween > 89) { if (cw90 > 0f && SpaceOnWall >= cw90 && SpaceOnPrevious >= cw90) { // Assez d'espace pour le caisson de coin mural -> "90". StartPoint = AiGet.WallStartPoint(Wallstring); EndPoint = AiGet.WallEndPoint(Wallstring); Delta = new() { StartPoint, EndPoint }; yield return StartCoroutine(AddWallCorners("90", Wallstring, Delta, SpaceOnWall)); } else if (SpaceOnPrevious >= 12f && !string.IsNullOrEmpty(_AI.CWDEADR)) { // Pas assez pour un vrai coin, mais >= 12" sur le mur précédent -> coin mort (W10) sur sa fin. StartPoint = AiGet.WallEndPoint(PreviousWallName); EndPoint = AiGet.WallStartPoint(PreviousWallName); Delta = new() { StartPoint, EndPoint }; yield return StartCoroutine(AddWallCorners("Dead_R", PreviousWallName, Delta, SpaceOnPrevious)); } } } } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } IEnumerator AddWallCorners(string Model, string WallNumber, List Delta,float SpaceWall_1) { // Section du coin (jonction = Delta[0]) désactivée -> pas de caisson de coin mural ici. if (!SectionAllowed(WallNumber, Delta[0], true)) yield break; // Modèle de coin non configuré pour ce magasin ("" / null) -> pas de placement (évite le crash UIT.Value). string cornerModel = Model == "90" ? _AI.CW90 : Model == "Dead_R" ? _AI.CWDEADR : Model == "Dead_L" ? _AI.CBDEADR : ""; if (string.IsNullOrEmpty(cornerModel)) yield break; print("From AddWallCorners Model=====" + Model); Vector3 direction = (Delta[1] - Delta[0]).normalized; Vector3 PositionToInstall; Vector3 Forward = -Get.o1(WallNumber).transform.forward.normalized; float angle = GetAngleFromXClockwise(Delta[0], Delta[1]); if (Model == "90") { PositionToInstall = Delta[0] + direction.normalized * 12; PositionToInstall += Forward * 12; PositionToInstall.y = _AI.WallHeightPosition; Set_TemporySC(_AI.CW90, WallNumber, PositionToInstall, angle, "F"); } if (Model == "Dead_R") { // Bord du caisson collé au coin (fond du mur) = demi-largeur réelle depuis Delta[0]. float deadW = UIT.ValueFloat(Library.Cabinet, _AI.CWDEADR, Header.A); if (deadW <= 0f) deadW = 24f; PositionToInstall = Delta[0] + direction.normalized * (deadW * 0.5f); PositionToInstall += Forward * 6; PositionToInstall.y = _AI.WallHeightPosition; Set_TemporySC(_AI.CWDEADR, WallNumber, PositionToInstall, angle, "F"); _G.SC[4] = "W10"; } if (Model == "Dead_L") { PositionToInstall = Delta[0] - direction.normalized * 12; PositionToInstall += Forward * 6; PositionToInstall.y = _AI.WallHeightPosition; Set_TemporySC(_AI.CBDEADR, WallNumber, PositionToInstall, angle, "F"); } _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?On_50_F1E6B7?Off_10_F1E6B7";//C[40] = "Off_150_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); yield return null; } static List GetWindowsOnWall(string wall) { List windows = new(); string wallW = wall.Replace("m", "w"); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][22] == wallW && _G.OBJs[i][4] == "wind") { GameObject go = Get.o2("SCENE", _G.OBJs[i][0]); if (go != null && go.transform.Find("PBLB") != null) windows.Add(go); } } return windows; } //------------------------------_TALL CABINET------------------------------------------------------------------ public void AddTALLCabinet() { Vector3 StartPoint; Vector3 EndPoint; Vector3 ObstacleStartPoint; Vector3 ObstacleEndPoint; float AvailableStart; float AvailableEnd; List DeltaStart; List DeltaEnd; // Largeur non figée ici : ChooseTallModel retient la colonne standard ou la petite selon la // place réellement disponible à chaque emplacement. if (WallSelected.Count == 1) { List Obstacles = AiGet.AllObjectsOnWall(WallSelected[0], "All", "All", _AI.CeilingLine, _AI.FloorLine); Obstacles.AddRange(GetWindowsOnWall(WallSelected[0])); StartPoint = AiGet.WallStartPoint(WallSelected[0]); EndPoint = AiGet.WallEndPoint(WallSelected[0]); DeltaStart = new() { StartPoint, EndPoint }; DeltaEnd = new() { EndPoint, StartPoint }; AvailableStart = Vector3.Distance(StartPoint, EndPoint); AvailableEnd = Vector3.Distance(EndPoint, StartPoint); if (Obstacles.Count > 0) { ObstacleStartPoint = PointAlign(Obstacles[0].transform.Find("PBLB").transform.position, -_G.HEIGHT * 0.5f); ObstacleEndPoint = PointAlign(Obstacles[^1].transform.Find("PBRB").transform.position, -_G.HEIGHT * 0.5f); DeltaStart = new() { StartPoint, ObstacleStartPoint }; DeltaEnd = new() { EndPoint, ObstacleEndPoint }; AvailableStart = Vector3.Distance(StartPoint, ObstacleStartPoint); AvailableEnd = Vector3.Distance(EndPoint, ObstacleEndPoint); } // Ouverture proche d'un coin sur le mur voisin : la colonne ne peut pas coller à ce coin. // Les colonnes comptent AUSSI les fenêtres : elles les recouvriraient. // DeltaEnd part de la FIN du mur, DeltaStart de son DÉBUT : chacun touche son coin. if (OpeningNearNextWallCorner(WallSelected[0], "Tall") && AvailableEnd > 0f) { DeltaEnd = new() { DeltaEnd[0] + CornerOpeningSetback * (DeltaEnd[1] - DeltaEnd[0]).normalized, DeltaEnd[1] }; AvailableEnd -= CornerOpeningSetback; } if (OpeningNearPreviousWallCorner(WallSelected[0], "Tall") && AvailableStart > 0f) { DeltaStart = new() { DeltaStart[0] + CornerOpeningSetback * (DeltaStart[1] - DeltaStart[0]).normalized, DeltaStart[1] }; AvailableStart -= CornerOpeningSetback; } string TallStart = ChooseTallModel(AvailableStart, out _); string TallEnd = ChooseTallModel(AvailableEnd, out _); if (!string.IsNullOrEmpty(TallStart)) AddTallSpot(WallSelected[0], DeltaStart, TallStart, AvailableStart); if (!string.IsNullOrEmpty(TallEnd)) AddTallSpot(WallSelected[0], DeltaEnd, TallEnd, AvailableEnd); } if (WallSelected.Count >= 1) { foreach (string Wallstring in WallSelected) { StartPoint = AiGet.WallStartPoint(Wallstring); EndPoint = AiGet.WallEndPoint(Wallstring); List Obstacles = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.CeilingLine, _AI.FloorLine); Obstacles.AddRange(GetWindowsOnWall(Wallstring)); int IndexWall = int.Parse(Wallstring[1..]) - 1; Obstacles.Sort((obj1, obj2) => { float sqrDist1 = (obj1.transform.position - StartPoint).sqrMagnitude; float sqrDist2 = (obj2.transform.position - StartPoint).sqrMagnitude; return sqrDist1.CompareTo(sqrDist2); }); DeltaStart = SetDeltaPoint(Wallstring, StartPoint, EndPoint, "Base",Obstacles); AvailableStart = Vector3.Distance(StartPoint, EndPoint); AvailableEnd = Vector3.Distance(EndPoint, StartPoint); DeltaEnd = new() { EndPoint, StartPoint }; bool isEndAvailable = false; if (EndPoint == DeltaStart[1]) { isEndAvailable = true; } if (Obstacles.Count > 0) { ObstacleStartPoint = PointAlign(Obstacles[0].transform.Find("PBLB").transform.position, -_G.HEIGHT * 0.5f); ObstacleEndPoint = PointAlign(Obstacles[^1].transform.Find("PBRB").transform.position, -_G.HEIGHT * 0.5f); DeltaStart = new() { StartPoint, ObstacleStartPoint }; DeltaEnd = new() { EndPoint, ObstacleEndPoint }; AvailableStart = Vector3.Distance(StartPoint, ObstacleStartPoint); AvailableEnd = Vector3.Distance(EndPoint, ObstacleEndPoint); } //if !previous bool isPreviousSpace = true; string WallPreviousName = AiGet.PreviousWallName(Wallstring).Replace("w","m"); if (WallSelected.Contains(WallPreviousName)) { isPreviousSpace = false; } bool isNexSpace = true; string WallNextName = AiGet.NextWallName(Wallstring).Replace("w","m"); if (WallSelected.Contains(WallNextName)) { isNexSpace = false; } // Idem sur les deux coins : la colonne concernée recule de 12". if (OpeningNearNextWallCorner(Wallstring, "Tall") && AvailableEnd > 0f) { DeltaEnd = new() { DeltaEnd[0] + CornerOpeningSetback * (DeltaEnd[1] - DeltaEnd[0]).normalized, DeltaEnd[1] }; AvailableEnd -= CornerOpeningSetback; } if (OpeningNearPreviousWallCorner(Wallstring, "Tall") && AvailableStart > 0f) { DeltaStart = new() { DeltaStart[0] + CornerOpeningSetback * (DeltaStart[1] - DeltaStart[0]).normalized, DeltaStart[1] }; AvailableStart -= CornerOpeningSetback; } string TallStart = ChooseTallModel(AvailableStart, out _); string TallEnd = ChooseTallModel(AvailableEnd, out _); if (!string.IsNullOrEmpty(TallStart) && isPreviousSpace) { AddTallSpot(Wallstring, DeltaStart, TallStart, AvailableStart); } if (isEndAvailable && !string.IsNullOrEmpty(TallEnd) && isNexSpace) { AddTallSpot(Wallstring, DeltaEnd, TallEnd, AvailableEnd); } } } // Les emplacements sont tous connus : on peut maintenant choisir lequel reçoit la péninsule. DispatchTallSpots(); // //print("Add Talls Cabinet"); // //Find Last cab _AI.BASE and last cab wall // GameObject LastBASECabinet; // GameObject LastWallCabinet; foreach (string Wallstring in WallSelected) { GameObject Fridge = GetObject("frid", Wallstring); if (Fridge != null) { string wallW = Wallstring.Replace("m", "w"); Vector3 DirectionFridge = Fridge.transform.right.normalized; Vector3 fridgeCenter = Fridge.transform.position; Vector3 wallStart = AiGet.WallStartPoint(Wallstring); // Find existing tall: remember which side of the fridge it was on bool removedOnLeft = true; // default: place left bool foundTall = false; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][22] == wallW && _G.OBJs[i][4] == "T1") { GameObject existingTall = Get.o2("SCENE", _G.OBJs[i][0]); if (existingTall != null) { // Dot > 0 means tall is to the RIGHT of fridge center removedOnLeft = Vector3.Dot(existingTall.transform.position - fridgeCenter, DirectionFridge) < 0; DestroyImmediate(existingTall); foundTall = true; } } } // Collect all obstacles + windows on the wall, sorted from wall start List allObs = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.CeilingLine, _AI.FloorLine); allObs.AddRange(GetWindowsOnWall(Wallstring)); allObs.Sort((a, b) => (a.transform.position - wallStart).sqrMagnitude.CompareTo((b.transform.position - wallStart).sqrMagnitude)); // Espace libre de CHAQUE côté du frigo, borné par l'obstacle le plus proche. Vector3 fridgeLeft = Fridge.transform.Find("PBLB").position; Vector3 fridgeRight = Fridge.transform.Find("PBRB").position; Vector3 wallEnd = AiGet.WallEndPoint(Wallstring); Vector3 wallStartLim = wallStart; // Les DEUX bouts du mur reculent de 12" quand une ouverture est proche du coin sur le mur // voisin (même règle que SetDeltaPoint). Ce bloc-ci ne passe pas par SetDeltaPoint : sans // ces reculs, la colonne posée à côté du frigo allait jusqu'au coin et empiétait sur // l'encadrement. "Tall" -> les fenêtres comptent aussi, une colonne les recouvrirait. Vector3 wallAxis = (wallEnd - wallStart).normalized; if (OpeningNearNextWallCorner(Wallstring, "Tall")) wallEnd -= CornerOpeningSetback * wallAxis; if (OpeningNearPreviousWallCorner(Wallstring, "Tall")) wallStartLim += CornerOpeningSetback * wallAxis; // Max(0, ...) et non Abs : avec le recul ci-dessus, le bout du mur peut se retrouver // DERRIÈRE le bord du frigo. Abs aurait alors rendu une place positive inexistante. float spaceLeft = Mathf.Max(0f, Vector3.Dot(wallStartLim - fridgeLeft, -DirectionFridge)); float spaceRight = Mathf.Max(0f, Vector3.Dot(wallEnd - fridgeRight, DirectionFridge)); foreach (GameObject obs in allObs) { if (obs == Fridge) continue; Transform pbrb = obs.transform.Find("PBRB"); if (pbrb != null) { float d = Vector3.Dot(fridgeLeft - pbrb.position, DirectionFridge); if (d > 0 && d < spaceLeft) spaceLeft = d; } Transform pblb = obs.transform.Find("PBLB"); if (pblb != null) { float d = Vector3.Dot(pblb.position - fridgeRight, DirectionFridge); if (d > 0 && d < spaceRight) spaceRight = d; } } // AVANT : les deux branches étaient conditionnées à foundTall, donc ce bloc ne savait que // DÉPLACER une colonne existante. Sans colonne posée ailleurs sur ce mur, il n'en arrivait // jamais à côté du frigo — même avec la place pour une TALLSMALL — et l'espace partait en // caissons bas + hauts. // Côté retenu : celui qui éloigne le PLUS la colonne du coin. Un coin existe au début du // mur (mur précédent sélectionné) et/ou à sa fin (mur suivant sélectionné). Sans aucun // coin — un seul mur — la référence devient le CENTRE du mur, la colonne part donc vers // un bout. La largeur disponible ne sert plus qu'à savoir ce qui RENTRE de chaque côté. // Égalité (moins de 1/2") -> le côté où une colonne existait déjà, qu'on vient de retirer. // On bascule sur l'autre côté si rien n'y rentre : sans ce repli, une colonne retirée // pouvait ne jamais être reposée. string modelLeft = ChooseTallModel(spaceLeft, out float wideLeft); string modelRight = ChooseTallModel(spaceRight, out float wideRight); Vector3 wStartRef = AiGet.WallStartPoint(Wallstring); Vector3 wEndRef = AiGet.WallEndPoint(Wallstring); Vector3 wDirRef = (wEndRef - wStartRef).normalized; float wLenRef = Vector3.Distance(wStartRef, wEndRef); List refs = new(); if (WallSelected.Contains(AiGet.PreviousWallName(Wallstring).Replace("w", "m"))) refs.Add(0f); if (WallSelected.Contains(AiGet.NextWallName(Wallstring).Replace("w", "m"))) refs.Add(wLenRef); if (refs.Count == 0) refs.Add(wLenRef * 0.5f); // un seul mur -> on s'éloigne du centre // Distance du CENTRE de la colonne à la référence la plus proche : plus c'est grand, // mieux c'est. float ScoreSide(bool left, float tallW, float space) { float g = Mathf.Min(_AI.FridgeGab, Mathf.Max(0f, space - tallW)); Vector3 c = left ? fridgeLeft - (g + tallW * 0.5f) * DirectionFridge : fridgeRight + (g + tallW * 0.5f) * DirectionFridge; float d = Vector3.Dot(c - wStartRef, wDirRef); float best = float.MaxValue; foreach (float r in refs) best = Mathf.Min(best, Mathf.Abs(d - r)); return best; } float scoreLeft = string.IsNullOrEmpty(modelLeft) ? float.MinValue : ScoreSide(true, wideLeft, spaceLeft); float scoreRight = string.IsNullOrEmpty(modelRight) ? float.MinValue : ScoreSide(false, wideRight, spaceRight); bool leftFirst = Mathf.Abs(scoreLeft - scoreRight) < 0.5f ? (foundTall ? removedOnLeft : scoreLeft >= scoreRight) : scoreLeft > scoreRight; for (int pass = 0; pass < 2; pass++) { bool left = (pass == 0) == leftFirst; float space = left ? spaceLeft : spaceRight; string tallModel = left ? modelLeft : modelRight; float tallW = left ? wideLeft : wideRight; if (string.IsNullOrEmpty(tallModel)) continue; // Jeu du frigo si la place le permet, sinon on colle. AVANT : 2" à gauche mais 0 à // droite, la colonne de droite touchait le frigo. float gap = Mathf.Min(_AI.FridgeGab, Mathf.Max(0f, space - tallW)); if (left) { EndPoint = fridgeLeft - gap * DirectionFridge; StartPoint = EndPoint - tallW * DirectionFridge; } else { StartPoint = fridgeRight + gap * DirectionFridge; EndPoint = StartPoint + tallW * DirectionFridge; } print("[AI] Colonne a " + (left ? "gauche" : "droite") + " du frigo sur " + Wallstring + " : " + tallModel + " (" + tallW + ") dans " + space + ", jeu " + gap + " ; eloignement gauche=" + scoreLeft + " droite=" + scoreRight); StartCoroutine(Add_TallCabinetonWall(Wallstring, new() { StartPoint, EndPoint }, tallModel)); break; } } } //DOIT.AllSelOff(); } // Modèle de colonne (garde-manger) qui entre dans 'available' : la colonne standard si elle rentre, // sinon la PETITE (_AI.TALLSMALL) quand le catalogue en déclare une. "" si rien ne rentre. static string ChooseTallModel(float available, out float wide) { wide = 0f; float w = UIT.ValueFloat(Library.Cabinet, _AI.TALL, Header.A); if (w > 0f && available >= w) { wide = w; return _AI.TALL; } if (!string.IsNullOrEmpty(_AI.TALLSMALL)) { float ws = UIT.ValueFloat(Library.Cabinet, _AI.TALLSMALL, Header.A); if (ws > 0f && available >= ws) { print("[AI] Colonne : " + available + " insuffisant pour " + _AI.TALL + " (" + w + ") -> " + _AI.TALLSMALL + " (" + ws + ")"); wide = ws; return _AI.TALLSMALL; } } return ""; } // Model : null -> colonne standard (_AI.TALL). Les dimensions (A / B / FF) sont TOUJOURS relues sur // le modèle retenu, sinon une petite colonne serait posée à la largeur de la grande. IEnumerator Add_TallCabinetonWall(string Wallstring, List Delta, string Model = null) { // Section désactivée pour ce mur -> pas de caisson haut ici (milieu du delta). if (!SectionAllowed(Wallstring, (Delta[0] + Delta[1]) * 0.5f)) yield break; if (string.IsNullOrEmpty(Model)) Model = _AI.TALL; _aiBusy++; Vector3 Direction = Delta[1] - Delta[0]; float TallWide = DOIT.ConvertStringToNumber(UIT.Value(Library.Cabinet, Model, Header.A)); float FromFloor = DOIT.ConvertStringToNumber(UIT.Value(Library.Cabinet, Model, Header.FF)); float CabinetHeight = DOIT.ConvertStringToNumber(UIT.Value(Library.Cabinet, Model, Header.B)); Vector3 PositionToInstall = Delta[0]+TallWide*0.5f*Direction.normalized; float angle = GetAngleByWall(Wallstring); Vector3 Forward = AiGet.FowardbyWall(Wallstring); PositionToInstall.y = -_G.HEIGHT * 0.5f + FromFloor+CabinetHeight*0.5f; //_AI.BaseHeightPosition; PositionToInstall += (_AI.BaseDepth+_AI.Door1Thickness)*0.5f* Forward; Set_TemporySC(Model, Wallstring, PositionToInstall, angle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); if (FromFloor > 24) { print("Add basecabinet"); CabinetHeight=DOIT.ConvertStringToNumber(UIT.Value(Library.Cabinet, _AI.TallUnder, Header.B)); PositionToInstall.y= -_G.HEIGHT * 0.5f + CabinetHeight*0.5f; Set_TemporySC(_AI.TallUnder, Wallstring, PositionToInstall, angle, "F"); _G.SC[14] = "0"; _G.SC[55] = "0,0,0,0"; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); ReflectionProbesUpdater.Instance.UpdateProbes(); _aiBusy--; yield return null; } //-------------------------------BASE CABINET------------------------------------------------------------------ public void AddBASECabinet() { StartCoroutine(CoroutineAdd_BASECabinet()); } IEnumerator CoroutineAdd_BASECabinet() { foreach (string Wallstring in WallsFillOrder()) // mur suivant (droite) d'abord -> détection du coin { yield return StartCoroutine(Add_BASECabinetonWall(Wallstring)); } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } IEnumerator Add_BASECabinetonWall(string Wallstring) { int IndexWall = int.Parse(Wallstring[1..]) - 1; List Obstacles = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.FloorLine + 10, _AI.FloorLine); //Points Vector3 StartPoint = _G.ExteriorWallPoints[IndexWall][0]; Vector3 EndPoint = _G.ExteriorWallPoints[IndexWall][1]; Obstacles.Sort((obj1, obj2) => { float sqrDist1 = (obj1.transform.position - StartPoint).sqrMagnitude; float sqrDist2 = (obj2.transform.position - StartPoint).sqrMagnitude; return sqrDist1.CompareTo(sqrDist2); }); foreach (GameObject O in Obstacles) { print("Obstacle===Base=" + O.name); } List ListDelta = SetDeltaPoint(Wallstring, StartPoint, EndPoint, "Base",Obstacles); print("Wall number ==============Base " + Wallstring+" Start Point====="+ListDelta[0]+" End Point====="+ListDelta[1]); //yield return null; //Add Cabinets yield return StartCoroutine(AddCabinetBaseToScene(Wallstring, ListDelta, Obstacles)); //yield return null; } IEnumerator AddCabinetBaseToScene(string Wallstring, List Delta,List Obstacles) { int DeltaQty = Obstacles.Count+1; Vector3 StartPoint = Delta[0]; Vector3 EndPoint; Vector3 WallEndPoint = Delta[1]; List ListObstacles = Obstacles; Vector3 Direction = (Delta[1] - Delta[0]).normalized; //if (CheckCornerAtEnd(Wallstring, "Base")) { Direction = -Direction; } if (DeltaQty == 1) { yield return AddBASECabinetOnDelta(Wallstring, Delta); yield return null; } if (DeltaQty >= 2) { //Start //if(Wallstring=="m6")print("ListObstacles[0]========on Wall 6 " + ListObstacles[0]); float ObjectGap = AiGet.Gap(ListObstacles[0].name); EndPoint = ListObstacles[0].transform.Find("PBLB").position - ObjectGap * Direction; Delta = new() { StartPoint, EndPoint }; print("Base Cabinet on Wall 6 " + Wallstring + "StartPoint====" + StartPoint + " EndPoint====" + EndPoint); // Le système de sections (AllowedSubDeltas) filtre déjà valide/non valide. On remplit donc // TOUT delta ; PlaceRun ignore seulement ce qui est < 9". Plus de skip lié aux ouvertures. if (!TallDeltaTooShort(ListObstacles[0], Delta)) yield return AddBASECabinetOnDelta(Wallstring, Delta); //Between for (int i = 3; i < DeltaQty +1; i++) { ObjectGap = AiGet.Gap(ListObstacles[i - 3].name); StartPoint = ListObstacles[i - 3].transform.Find("PBRB").position + ObjectGap * Direction; ObjectGap = AiGet.Gap(ListObstacles[i - 2].name); EndPoint = ListObstacles[i - 2].transform.Find("PBLB").position - ObjectGap * Direction; Delta = new() { StartPoint, EndPoint }; if (!TallDeltaTooShort(ListObstacles[i - 3], Delta) && !TallDeltaTooShort(ListObstacles[i - 2], Delta)) yield return AddBASECabinetOnDelta(Wallstring, Delta); } //End GameObject LastObstacle = ListObstacles[DeltaQty - 2]; ObjectGap = AiGet.Gap(LastObstacle.name); StartPoint = LastObstacle.transform.Find("PBRB").position + ObjectGap * Direction; EndPoint = WallEndPoint; Delta = new() { StartPoint, EndPoint }; if (!TallDeltaTooShort(LastObstacle, Delta)) yield return AddBASECabinetOnDelta(Wallstring, Delta); } yield return null; } // Wrapper sections : découpe le delta en sous-deltas autorisés (coupe le run à la fenêtre si // le sous-fenêtre est désactivé), puis remplit chacun. Mur sans restriction -> delta entier. IEnumerator AddBASECabinetOnDelta(string WallNumber, List Delta) { foreach (List sub in AllowedSubDeltas(WallNumber, Delta[0], Delta[1], false)) yield return PlaceBaseRun(WallNumber, sub); } IEnumerator PlaceBaseRun(string WallNumber, List Delta) { //print("Base wall ====2====" + WallNumber); Vector3 StartPoint = PointAlign(Delta[0], -_G.HEIGHT * 0.5f); Vector3 EndPoint = PointAlign(Delta[1], -_G.HEIGHT * 0.5f); float Distance = Vector3.Distance(StartPoint, EndPoint); print("Add_AI.BASECabinetOnDelta on wall " + WallNumber + " start==" + Delta[0] + " end==" + Delta[1] + " distance= " + Distance); // Travée réservée à la projection du comptoir d'une péninsule : on n'y pose rien, sinon un // caisson de 12" se retrouve sous le débord des tabourets. if (InPeninsulaReserve(WallNumber, StartPoint, EndPoint)) { print("[AI] Rangee " + WallNumber + " : travee de " + Distance + " reservee a la projection du comptoir de la peninsule -> laissee vide."); yield break; } if (Distance > 9) { Vector3 PositionToInstall = StartPoint; var direction = Get.o1(WallNumber).transform.right.normalized; float angle = GetAngleFromXClockwise(StartPoint, EndPoint); Vector3 Forward = -Get.o1(WallNumber).transform.forward.normalized; // Section à UN SEUL caisson (< 33") : sous fenêtre -> centré ; sinon collé vers la FIN. if (Distance < 33f) { bool underWin = IsUnderWindowDelta(WallNumber, StartPoint, EndPoint); PlaceSingleBase(WallNumber, StartPoint, EndPoint, Distance, direction, Forward, angle, underWin); } else { //Add cabinets float Qty = Mathf.Floor(Distance / 24f); PositionToInstall += 0.5f * (_AI.BaseDepth+_AI.Door1Thickness) * Forward; //List AddOnDelta = new(); string last = null; for (int i = 0; i < Qty; i++) { if (i == 0) { PositionToInstall += direction.normalized * 12; } PositionToInstall.y = _AI.BaseHeightPosition; Set_TemporySC(_AI.BASE, WallNumber, PositionToInstall, angle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; CreateCAB.AddCab(); last = _G.OBJs[_G.OBJnum - 1][0]; yield return null; if (i != Qty - 1) { PositionToInstall += direction * 24; } if (i == Qty - 1) { PositionToInstall += direction * 12; } } //Find Empty Space float EmptySpace = Distance - Qty * 24; print("EmptySpace=====" + EmptySpace); GameObject LastInstall = null; if (last != null) { LastInstall = Get.o2("SCENE", last); } if (EmptySpace >= 3 && EmptySpace < 9 && LastInstall != null)//Between 9 to 24 { if (EmptySpace >= 3 && EmptySpace < 6 && _AI.Base27 != "") { IncreaseBaseCab(_AI.Base27, LastInstall, 27, direction, angle, WallNumber); } else if (EmptySpace >= 6 && EmptySpace < 9 && _AI.Base30 != "") { IncreaseBaseCab(_AI.Base30, LastInstall, 30, direction, angle, WallNumber); } } else if (EmptySpace >= 9) { string AddCaB = ""; float Wide = 0; if (EmptySpace >= 9 && EmptySpace < 12 && _AI.Base9 != "") { AddCaB = _AI.Base9; Wide = 9; }//or IncreaseCab("B33") else if (EmptySpace >= 12 && EmptySpace < 15 && _AI.Base12 != "") { AddCaB = _AI.Base12; Wide = 12; }//or IncreaseCab("B36") else if (EmptySpace >= 15 && EmptySpace < 18 && _AI.Base15 != "") { AddCaB = _AI.Base15; Wide = 15; } else if (EmptySpace >= 18 && EmptySpace < 21 && _AI.Base18 != "") { AddCaB = _AI.Base18; Wide = 18; } else if (EmptySpace >= 21 && EmptySpace < 24 && _AI.Base21 != "") { AddCaB = _AI.Base21; Wide = 21; } else if (EmptySpace >= 21 && EmptySpace < 24 && _AI.Base18 != "") { AddCaB = _AI.Base18; Wide = 18; } if (AddCaB != "") { AddNewBaseCab(AddCaB, PositionToInstall, Wide, direction, angle, WallNumber); } } } // fin else : section à plusieurs caissons } yield return null; } // Section ne pouvant contenir qu'un seul caisson. Par défaut on le colle vers la FIN de la section. // centered = true (sous une fenêtre) -> on le centre plutôt dans la section. static void PlaceSingleBase(string WallNumber, Vector3 StartPoint, Vector3 EndPoint, float Distance, Vector3 direction, Vector3 Forward, float angle, bool centered) { string cab = _AI.BASE; float Wide = 24f; if (Distance >= 30f && !string.IsNullOrEmpty(_AI.Base30)) { cab = _AI.Base30; Wide = 30f; } else if (Distance >= 27f && !string.IsNullOrEmpty(_AI.Base27)) { cab = _AI.Base27; Wide = 27f; } else if (Distance >= 24f) { cab = _AI.BASE; Wide = 24f; } else if (Distance >= 21f && !string.IsNullOrEmpty(_AI.Base21)) { cab = _AI.Base21; Wide = 21f; } else if (Distance >= 18f && !string.IsNullOrEmpty(_AI.Base18)) { cab = _AI.Base18; Wide = 18f; } else if (Distance >= 15f && !string.IsNullOrEmpty(_AI.Base15)) { cab = _AI.Base15; Wide = 15f; } else if (Distance >= 12f && !string.IsNullOrEmpty(_AI.Base12)) { cab = _AI.Base12; Wide = 12f; } else if (Distance >= 9f && !string.IsNullOrEmpty(_AI.Base9)) { cab = _AI.Base9; Wide = 9f; } if (string.IsNullOrEmpty(cab)) return; // Sous fenêtre -> centré dans la section ; sinon bord éloigné collé à la fin (centre = fin - demi-largeur). Vector3 pos = centered ? (StartPoint + EndPoint) * 0.5f + 0.5f * (_AI.BaseDepth+_AI.Door1Thickness) * Forward : EndPoint + 0.5f * (_AI.BaseDepth+_AI.Door1Thickness) * Forward - (Wide * 0.5f) * direction; pos.y = _AI.BaseHeightPosition; Set_TemporySC(cab, WallNumber, pos, angle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; CreateCAB.AddCab(); } static void IncreaseBaseCab(string NewCab, GameObject LastInstall, float Wide, Vector3 direction,float angle,string WallNumber) { Vector3 PositionToInstall = LastInstall.transform.position; int ADN = Get.GetObjectIndex(LastInstall.name); _G.OBJs[ADN][0] = "null"; Destroy(LastInstall); PositionToInstall += (Wide - 24) * 0.5f * direction; PositionToInstall.y = _AI.BaseHeightPosition; Set_TemporySC(NewCab, WallNumber, PositionToInstall, angle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; CreateCAB.AddCab(); } static void AddNewBaseCab(string NewCab,Vector3 PositionToInstall,float Wide,Vector3 direction,float angle,string WallNumber) { print("Pass AddNewBaseCab " + NewCab); PositionToInstall += (0+(Wide*0.5f))*direction; PositionToInstall.y = _AI.BaseHeightPosition; // "F" et non "R" : Set_TemporySC résout alors le côté depuis la librairie, comme pour tous les // autres caissons AI. Forcé en "R", ce caisson de FINITION naissait déjà retourné — sa recette // de porte était inversée une fois par ConstructBASE — alors que les caissons orientés ensuite // par CoroutineFlipHandles passent par un chemin différent. Les deux familles ne s'affichaient // pas pareil pour un même ADN[23], d'où les bouts de section à l'envers. Set_TemporySC(NewCab, WallNumber, PositionToInstall, angle, "F"); _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; _G.SC[55] = "0,0,0,0"; CreateCAB.AddCab(); } //-------------------------------WALL CABINET------------------------------------------------------------------ public void AddWallCabinet() { StartCoroutine(CoroutineAddWallCabinet()); } IEnumerator CoroutineAddWallCabinet() { foreach (string Wallstring in WallsFillOrder()) // mur suivant (droite) d'abord -> détection du coin { yield return StartCoroutine(AddWallCabinetonWall(Wallstring)); } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } IEnumerator AddWallCabinetonWall(string Wallstring) { int IndexWall = int.Parse(Wallstring[1..]) - 1; //Points Vector3 StartPoint = _G.ExteriorWallPoints[IndexWall][0]; Vector3 EndPoint = _G.ExteriorWallPoints[IndexWall][1]; List Obstacles = AiGet.AllObjectsOnWall(Wallstring, "All", "All", _AI.CeilingLine, _AI.FloorLine + 64); Obstacles.Sort((obj1, obj2) => { float sqrDist1 = (obj1.transform.position - StartPoint).sqrMagnitude; float sqrDist2 = (obj2.transform.position - StartPoint).sqrMagnitude; return sqrDist1.CompareTo(sqrDist2); }); foreach (GameObject O in Obstacles) { print("Obstacle===Wall=" + O.name); } List ListDelta = SetDeltaPoint(Wallstring, StartPoint, EndPoint,"Wall",Obstacles); print("Wall number ==============Wall " + Wallstring+" Start Point====="+ListDelta[0]+" End Point====="+ListDelta[1]); //Add Cabinets yield return StartCoroutine(AddCabineWallToScene(Wallstring, ListDelta, Obstacles)); yield return null; } IEnumerator AddCabineWallToScene(string Wallstring, List Delta,List Obstacles) { int DeltaQty = Obstacles.Count+1; Vector3 DeltaStartPoint = Delta[0]; Vector3 StartPoint; Vector3 EndPoint; Vector3 DeltaEndPoint = Delta[1]; List ListObstacles = Obstacles; Vector3 Direction = (Delta[1] - Delta[0]).normalized; Vector3 WallStartPointReal = AiGet.WallStartPoint(Wallstring); Vector3 WallEndPointReal = AiGet.WallEndPoint(Wallstring); if (DeltaQty == 1) { yield return AddWallCabinetOnDelta(Wallstring, Delta); yield return null; } if (DeltaQty >= 2) { //Start float ObjectGap = AiGet.Gap(ListObstacles[0].name); EndPoint = PointAlign(ListObstacles[0].transform.Find("PBLB").position - ObjectGap * Direction,_AI.FloorLine); Delta = new() { DeltaStartPoint, EndPoint }; //Check if Obstacle is inDelta from start // float Distance_RealStart_DeltaStart = Vector3.Distance(WallStartPointReal, Delta[0]); // float Distance_RealStart_Obstacle = Vector3.Distance(WallStartPointReal, DeltaStartPoint); // if (Distance_RealStart_DeltaStart < Distance_RealStart_Obstacle) { yield return AddWallCabinetOnDelta(Wallstring, Delta); } // Sections (AllowedSubDeltas) filtrent déjà valide/non valide -> on remplit tout delta. if (!TallDeltaTooShort(ListObstacles[0], Delta)) yield return AddWallCabinetOnDelta(Wallstring, Delta); //Between for (int i = 3; i < DeltaQty + 1; i++)//3 & 4 { if (Wallstring == "m4") { print("ListObstacles[i - 3].name=====" + ListObstacles[i - 3].name); } ObjectGap = AiGet.Gap(ListObstacles[i - 3].name); StartPoint = ListObstacles[i - 3].transform.Find("PBRB").position + ObjectGap * Direction; ObjectGap = AiGet.Gap(ListObstacles[i - 2].name); EndPoint = ListObstacles[i - 2].transform.Find("PBLB").position - ObjectGap * Direction; Delta = new() { StartPoint, EndPoint }; if (!TallDeltaTooShort(ListObstacles[i - 3], Delta) && !TallDeltaTooShort(ListObstacles[i - 2], Delta)) yield return AddWallCabinetOnDelta(Wallstring, Delta); } //End GameObject LastObstacle = ListObstacles[DeltaQty - 2]; if (!TallDeltaTooShortAtEnd(LastObstacle, Direction, DeltaEndPoint)) { ObjectGap = AiGet.Gap(LastObstacle.name); StartPoint = LastObstacle.transform.Find("PBRB").position + ObjectGap * Direction; EndPoint = DeltaEndPoint; Delta = new() { StartPoint, EndPoint }; // Garde : ne pas dépasser la fin réelle du mur. Le remplissage est sinon inconditionnel. float Distance_RealEnd_DeltaEnd = Vector3.Distance(WallEndPointReal, DeltaEndPoint); float Distance_RealEnd_Obstacle = Vector3.Distance(WallEndPointReal, EndPoint); if (Distance_RealEnd_DeltaEnd <= Distance_RealEnd_Obstacle) yield return AddWallCabinetOnDelta(Wallstring, Delta); } } yield return null; } // Wrapper sections : isWallCab=true -> les caissons hauts sautent toute section sous-fenêtre. static IEnumerator AddWallCabinetOnDelta(string WallNumber, List Delta) { foreach (List sub in AllowedSubDeltas(WallNumber, Delta[0], Delta[1], true)) yield return PlaceWallRun(WallNumber, sub); } static IEnumerator PlaceWallRun(string WallNumber, List Delta) { Vector3 StartPoint = PointAlign(Delta[0], -_G.HEIGHT * 0.5f); Vector3 EndPoint = PointAlign(Delta[1], -_G.HEIGHT * 0.5f); float Distance = Vector3.Distance(StartPoint, EndPoint); if(WallNumber=="m4")print("Distance======" + Distance); //if(WallNumber=="m3")print("m3 Delta[0]===" + Delta[0] + "Delta[1]===" + Delta[1] +" distance===="+Distance); if (Distance > 9) { Vector3 PositionToInstall = StartPoint; var direction = Get.o1(WallNumber).transform.right.normalized; //Rotation float angle = GetAngleFromXClockwise(StartPoint, EndPoint); Vector3 Forward = -Get.o1(WallNumber).transform.forward.normalized; // Section à UN SEUL caisson (< 33") : le coller vers la FIN de la section (bord éloigné au bout). if (Distance < 33f) { PlaceSingleWallAtEnd(WallNumber, EndPoint, Distance, direction, Forward, angle); } else { //Add cabinets float Qty = Mathf.Floor(Distance / 24f); PositionToInstall += Forward * (_AI.WallDepth+_AI.Door1Thickness) * 0.5f; string last =null; for (int i = 0; i < Qty; i++) { if (i == 0) { PositionToInstall += direction.normalized * 12; } PositionToInstall.y = _AI.WallHeightPosition; Set_TemporySC(_AI.WALL, WallNumber, PositionToInstall, angle, "F"); _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); last=_G.OBJs[_G.OBJnum - 1][0]; yield return null; if (i != Qty - 1) { PositionToInstall += direction * 24; } if (i == Qty - 1) { PositionToInstall += direction * 12; } } //Find Empty Space float EmptySpace = Distance - Qty * 24; GameObject LastInstall = null; if (last !=null) { LastInstall = Get.o2("SCENE", last); } if (EmptySpace >= 3 && EmptySpace < 9 && LastInstall != null) { if (EmptySpace >= 3 && EmptySpace < 6 && _AI.Wall27 != "") { IncreaseWallCab(_AI.Wall27, LastInstall, 27, direction, angle, WallNumber); } else if (EmptySpace >= 6 && EmptySpace < 9 && _AI.Wall30 != "") { IncreaseWallCab(_AI.Wall30, LastInstall, 30, direction, angle, WallNumber); } } else if (EmptySpace >= 9) { string AddCaB = ""; float Wide = 0; if (EmptySpace >= 9 && EmptySpace < 12 && _AI.Wall9 != "") { AddNewWallCab(_AI.Wall9, PositionToInstall, 9, direction, angle, WallNumber); }//or IncreaseCab("W3330") else if (EmptySpace >= 12 && EmptySpace < 15 && _AI.Wall12 != "") { AddCaB = _AI.Wall12; Wide = 12; }//or IncreaseCab("W3630") else if (EmptySpace >= 15 && EmptySpace < 18 && _AI.Wall15 != "") { AddCaB = _AI.Wall15; Wide = 15; } else if (EmptySpace >= 18 && EmptySpace < 21 && _AI.Wall18 != "") { AddCaB = _AI.Wall18; Wide = 18; } else if (EmptySpace >= 21 && EmptySpace < 24 && _AI.Wall21 != "") { AddCaB = _AI.Wall21; Wide = 21; } else if (EmptySpace >= 21 && EmptySpace < 24 && _AI.Wall18 != "") { AddCaB = _AI.Wall18; Wide = 18; } if (AddCaB != "") { AddNewWallCab(AddCaB, PositionToInstall, Wide, direction, angle, WallNumber); } } } // fin else : section à plusieurs caissons } yield return null; } // Section ne pouvant contenir qu'un seul caisson haut : on le colle vers la FIN de la section. static void PlaceSingleWallAtEnd(string WallNumber, Vector3 EndPoint, float Distance, Vector3 direction, Vector3 Forward, float angle) { string cab = _AI.WALL; float Wide = 24f; if (Distance >= 30f && !string.IsNullOrEmpty(_AI.Wall30)) { cab = _AI.Wall30; Wide = 30f; } else if (Distance >= 27f && !string.IsNullOrEmpty(_AI.Wall27)) { cab = _AI.Wall27; Wide = 27f; } else if (Distance >= 24f) { cab = _AI.WALL; Wide = 24f; } else if (Distance >= 21f && !string.IsNullOrEmpty(_AI.Wall21)) { cab = _AI.Wall21; Wide = 21f; } else if (Distance >= 18f && !string.IsNullOrEmpty(_AI.Wall18)) { cab = _AI.Wall18; Wide = 18f; } else if (Distance >= 15f && !string.IsNullOrEmpty(_AI.Wall15)) { cab = _AI.Wall15; Wide = 15f; } else if (Distance >= 12f && !string.IsNullOrEmpty(_AI.Wall12)) { cab = _AI.Wall12; Wide = 12f; } else if (Distance >= 9f && !string.IsNullOrEmpty(_AI.Wall9)) { cab = _AI.Wall9; Wide = 9f; } if (string.IsNullOrEmpty(cab)) return; // Bord éloigné collé à la fin de la section : centre = fin - demi-largeur. Vector3 pos = EndPoint + Forward * (_AI.WallDepth+_AI.Door1Thickness) * 0.5f - (Wide * 0.5f) * direction; pos.y = _AI.WallHeightPosition; Set_TemporySC(cab, WallNumber, pos, angle, "F"); _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); } static void IncreaseWallCab(string NewCab, GameObject LastInstall, float Wide, Vector3 direction,float angle,string WallNumber) { Vector3 PositionToInstall = LastInstall.transform.position; int ADN = Get.GetObjectIndex(LastInstall.name); _G.OBJs[ADN][0] = "null"; Destroy(LastInstall); PositionToInstall += (Wide - 24) * 0.5f * direction; PositionToInstall.y = _AI.WallHeightPosition; Set_TemporySC(NewCab, WallNumber, PositionToInstall, angle, "F"); _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); } static void AddNewWallCab(string NewCab,Vector3 PositionToInstall,float Wide,Vector3 direction,float angle,string WallNumber) { PositionToInstall += Wide*0.5f*direction; PositionToInstall.y = _AI.WallHeightPosition; // "F" et non "R" : même raison que dans AddNewBaseCab, le côté vient de la librairie. Set_TemporySC(NewCab, WallNumber, PositionToInstall, angle, "F"); _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); } //-------------------------------ISLAND CABINET------------------------------------------------------------------ // Dégagements (pouces) : 36" mini obligatoire autour de l'îlot, 42" devant les appareils. const float ISLAND_MIN_CLEAR = 36f; const float ISLAND_APPLIANCE_CLEAR = 42f; const float ISLAND_GRID_STEP = 2f; // résolution de la carte d'occupation (pouces) const float ISLAND_MIN_LEN = 24f; // longueur minimale d'un îlot exploitable static readonly float[] ISLAND_STANDOFFS = { 48f, 42f, 36f }; // distance visée au comptoir mural (préférence décroissante) public void AddIslandCabinet() { StartCoroutine(AddIslandCabinetonFloor()); } // Refonte : place un îlot (rangée d'armoires de base + panneau arrière) dans le plus grand // rectangle libre du sol respectant les dégagements, orienté selon le mur au plus fort poids // fonctionnel (évier / cuisson / préparation). Si aucune taille valide ne tient -> aucun îlot. IEnumerator AddIslandCabinetonFloor() { // Le design ne demande pas d'îlot : rien à placer. // Seul le type ÎLOT passe par ici. Une péninsule est rattachée au mur : elle est construite par // AddPeninsulaAt, à l'emplacement où une colonne aurait été posée. if (!IslandTypeIs("Island")) { print("[AI] Island = " + _G.Island + " (type " + IslandType() + ") -> pas d'ilot libre."); yield break; } foreach(string Optionname in _G.OPTIONS) { print("Option is ========="+ Optionname); } // 1) Mur de référence pour l'orientation (plus fort poids fonctionnel) string funcWall = ChooseFunctionalWall(); // 2) Carte d'occupation du sol (murs + emprises armoires/appareils + dégagements) BuildOccupancyGrid(out bool[,] occ, out int cols, out int rows, out float originX, out float originZ); // 3) Plus grand rectangle libre if (LargestFreeRectangle(occ, cols, rows, out int bC, out int bR, out int bW, out int bH)) { float step = ISLAND_GRID_STEP; // Bornes MONDE du rectangle libre. float rMinX = originX + bC * step; float rMaxX = originX + (bC + bW) * step; float rMinZ = originZ + bR * step; float rMaxZ = originZ + (bR + bH) * step; // 4) Mur à faire face (mur 4) : mur au run de caissons le plus long (devant/portes parallèles). string mur4 = ChooseIslandWall(funcWall); bool lengthAlongX = WallAlongX(mur4); // Ajustement : clipper la zone à l'ÉTENDUE des runs de caissons (lignes d'ajustement) : // longueur limitée au run de mur 4, profondeur limitée au run perpendiculaire. ClipZoneToCabinetRuns(mur4, lengthAlongX, ref rMinX, ref rMaxX, ref rMinZ, ref rMaxZ); float freeX = rMaxX - rMinX; float freeZ = rMaxZ - rMinZ; float centerX = (rMinX + rMaxX) * 0.5f; float centerZ = (rMinZ + rMaxZ) * 0.5f; // Visualisation de la zone îlot disponible (rectangle rouge à 1" du sol) — désactivée. // Décommenter pour vérifier la zone : //DrawIslandZone(rMinX, rMaxX, rMinZ, rMaxZ, -_G.HEIGHT * 0.5f + 1f); float freeLen = lengthAlongX ? freeX : freeZ; float freeDepth = lengthAlongX ? freeZ : freeX; // 5) Composer la longueur avec des caissons de largeurs variées (<= run de mur 4 = freeLen). // Aucun caisson évier contre un mur -> l'îlot doit le porter. // L'îlot prend l'évier dès qu'il n'y en a pas au mur — c'est exactement le cas quand // "Island = Sink" a fait renoncer AddSinkCabinet. Ne PAS forcer sur IslandTakesSink() ici : // si l'îlot avait été jugé impossible, un évier mural a été posé et on en aurait deux. bool sinkInIsland = !SinkOnWallExists(); var islandCabs = BuildIslandCabinets(freeLen, sinkInIsland); int count = islandCabs.Count; float islandLen = 0f; foreach (var cab in islandCabs) islandLen += cab.w; float baseDepth = (_AI.BaseDepth+_AI.Door1Thickness); // Type d'îlot : l'option "IslandDouble" du design demande 2 rangées dos-à-dos. Sans elle, // îlot SIMPLE = une seule rangée, pas de rangée arrière. Même avec l'option, on ne l'ajoute // que si la profondeur disponible le permet (>= 2 profondeurs de base). bool wantDouble = _G.OPTIONS != null && _G.OPTIONS.Contains("IslandDouble"); bool doubleRow = wantDouble && freeDepth >= 2f * baseDepth + 1f; float islandDepth = doubleRow ? 2f * baseDepth : baseDepth + 0.5f; // Distance à viser du comptoir mural : le bord du rectangle libre est déjà à 36" du comptoir, // on choisit la plus grande distance standard (48>42>36) qui laisse encore l'îlot dans l'espace. float maxWallClear = ISLAND_MIN_CLEAR + (freeDepth - islandDepth); float wallClear = ISLAND_MIN_CLEAR; foreach (float c in ISLAND_STANDOFFS) if (c <= maxWallClear + 0.01f) { wallClear = c; break; } // 6) Vérification : tout doit tenir dans le rectangle libre (dégagements déjà garantis // par construction de la grille). Sinon -> pas d'îlot. if (count >= 1 && islandLen >= ISLAND_MIN_LEN && freeDepth >= islandDepth) { Vector3 axis = lengthAlongX ? Vector3.right : Vector3.forward; Vector3 depthAxis = lengthAlongX ? Vector3.forward : Vector3.right; // Direction (le long de l'axe profondeur) qui pointe vers le mur 4 (comptoir mural). Vector3 u = depthAxis; if (!string.IsNullOrEmpty(mur4)) { GameObject w = Get.o1(mur4); if (w != null) { Vector3 toWall = w.transform.position - new Vector3(centerX, 0f, centerZ); if (Vector3.Dot(new Vector3(toWall.x, 0f, toWall.z), depthAxis) < 0f) u = -depthAxis; } } // Façades projetées sur u (u pointe vers le mur) : // - counterFrontU = ligne de comptoir (caissons de BASE) = référence pour le 48/42/36. // - protrudeFrontU = objet le plus avancé (appareils compris) = sécurité dégagement mini. float counterFrontU = float.NaN; float protrudeFrontU = float.NaN; if (!string.IsNullOrEmpty(mur4)) { for (int oi = 0; oi < _G.OBJnum; oi++) { if (!DOIT.exist(oi)) continue; string[] o = _G.OBJs[oi]; if (o == null || o.Length < 23) continue; if (o[22] != mur4) continue; if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; // ignorer les caissons muraux hauts float ocx = DOIT.ConvertStringToNumber(o[15]); float ocz = DOIT.ConvertStringToNumber(o[17]); float od = DOIT.ConvertStringToNumber(o[8]); float frontU = Vector3.Dot(new Vector3(ocx, 0f, ocz), u) - od * 0.5f; // face vers la pièce // protrude = face la plus PROCHE de l'îlot (caisson de coin saillant) = sécurité 36. if (float.IsNaN(protrudeFrontU) || frontU < protrudeFrontU) protrudeFrontU = frontU; // counter = ligne de comptoir du run = face la plus LOIN (caissons base/tall alignés au mur). bool runCab = o[4].Length >= 1 && (o[4][0] == 'B' || o[4][0] == 'T'); if (runCab && (float.IsNaN(counterFrontU) || frontU > counterFrontU)) counterFrontU = frontU; } } if (float.IsNaN(counterFrontU)) counterFrontU = protrudeFrontU; // pas de base -> repli Vector3 center = new Vector3(centerX, _AI.BaseHeightPosition, centerZ); float centerU0 = Vector3.Dot(new Vector3(centerX, 0f, centerZ), u); if (!float.IsNaN(counterFrontU)) { // Face portes à 'wallClear' de la ligne de comptoir... float doorU = counterFrontU - wallClear; // ...mais jamais à moins de 36" d'un objet saillant (appareil profond, etc.). if (!float.IsNaN(protrudeFrontU) && protrudeFrontU - doorU < ISLAND_MIN_CLEAR) doorU = protrudeFrontU - ISLAND_MIN_CLEAR; float targetCenterU = doorU - islandDepth * 0.5f; center += u * (targetCenterU - centerU0); } else { // Aucun caisson identifié sur le mur fonctionnel : repli sur le décalage relatif au rectangle. float depthOffset = freeDepth * 0.5f + ISLAND_MIN_CLEAR - wallClear - islandDepth * 0.5f; center += u * depthOffset; } // --- Positionnement LATÉRAL : 48/42/36 du/des mur(s) PERPENDICULAIRE(S) à caissons (le coin) --- // On ne considère que les caissons des murs perpendiculaires à mur 4 (les vrais côtés). { float centerA = Vector3.Dot(new Vector3(center.x, 0f, center.z), axis); float halfL = islandLen * 0.5f; // runPlus/runMinus = ligne de comptoir (face la plus LOIN de l'îlot) = référence 48/42/36. // protrudePlus/Minus = face la plus PROCHE (caisson de coin qui dépasse) = sécurité >=36. float runPlus = float.NaN, protrudePlus = float.PositiveInfinity; float runMinus = float.NaN, protrudeMinus = float.NegativeInfinity; for (int oi = 0; oi < _G.OBJnum; oi++) { if (!DOIT.exist(oi)) continue; string[] o = _G.OBJs[oi]; if (o == null || o.Length < 23) continue; string c4 = o[4]; if (c4 == "wind" || c4 == "door" || c4 == "open" || c4 == "pati") continue; if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; // caissons muraux hauts string ow22 = o[22]; if (string.IsNullOrEmpty(ow22) || ow22 == "floor") continue; if (WallAlongX(ow22) == lengthAlongX) continue; // uniquement les murs PERPENDICULAIRES à mur 4 float ocx = DOIT.ConvertStringToNumber(o[15]); float ocz = DOIT.ConvertStringToNumber(o[17]); float ow = DOIT.ConvertStringToNumber(o[6]); float od = DOIT.ConvertStringToNumber(o[8]); if (ow <= 0f || od <= 0f) continue; float oang = DOIT.ConvertStringToNumber(o[19]) * Mathf.Deg2Rad; Vector3 oc = new Vector3(ocx, 0f, ocz); float exAxis = lengthAlongX ? Mathf.Abs(Mathf.Cos(oang)) * ow * 0.5f + Mathf.Abs(Mathf.Sin(oang)) * od * 0.5f : Mathf.Abs(Mathf.Sin(oang)) * ow * 0.5f + Mathf.Abs(Mathf.Cos(oang)) * od * 0.5f; float pa = Vector3.Dot(oc, axis); bool runCab = c4.Length >= 1 && (c4[0] == 'B' || c4[0] == 'T'); // caisson (base/tall) = ligne de comptoir if (pa > centerA) { float face = pa - exAxis; if (face < protrudePlus) protrudePlus = face; // le plus proche if (runCab && (float.IsNaN(runPlus) || face > runPlus)) runPlus = face; // le plus loin } else { float face = pa + exAxis; if (face > protrudeMinus) protrudeMinus = face; // le plus proche if (runCab && (float.IsNaN(runMinus) || face < runMinus)) runMinus = face; // le plus loin } } if (float.IsNaN(runPlus) && !float.IsInfinity(protrudePlus)) runPlus = protrudePlus; if (float.IsNaN(runMinus) && !float.IsInfinity(protrudeMinus)) runMinus = protrudeMinus; // Écarts (jusqu'à la ligne de comptoir) de chaque côté float gapPlus = float.IsNaN(runPlus) ? float.PositiveInfinity : runPlus - (centerA + halfL); float gapMinus = float.IsNaN(runMinus) ? float.PositiveInfinity : (centerA - halfL) - runMinus; bool priorityPlus = gapPlus <= gapMinus; float priGap = priorityPlus ? gapPlus : gapMinus; float othGap = priorityPlus ? gapMinus : gapPlus; if (!float.IsInfinity(priGap)) { // Viser 48 (puis 42, 36) de la ligne de comptoir en gardant >=36 de l'autre côté. float target = ISLAND_MIN_CLEAR; foreach (float c in ISLAND_STANDOFFS) if (othGap + (priGap - c) >= ISLAND_MIN_CLEAR - 0.01f) { target = c; break; } float shiftAmt = priGap - target; // + = rapprocher du comptoir center += (priorityPlus ? axis : -axis) * shiftAmt; // Sécurité : >=36" du caisson de coin qui dépasse, du côté prioritaire. float ca2 = Vector3.Dot(new Vector3(center.x, 0f, center.z), axis); if (priorityPlus && !float.IsInfinity(protrudePlus)) { float over = 36f - (protrudePlus - (ca2 + halfL)); if (over > 0f) center -= axis * over; } else if (!priorityPlus && !float.IsInfinity(protrudeMinus)) { float over = 36f - ((ca2 - halfL) - protrudeMinus); if (over > 0f) center += axis * over; } } } // SÉCURITÉ : garder tout l'îlot À L'INTÉRIEUR de la zone libre (jamais hors de la pièce). // La zone libre est déjà entièrement dans la pièce (cellules hors polygone = occupées). { float halfX = lengthAlongX ? islandLen * 0.5f : islandDepth * 0.5f; float halfZ = lengthAlongX ? islandDepth * 0.5f : islandLen * 0.5f; float cx = center.x, cz = center.z; cx = (rMaxX - halfX >= rMinX + halfX) ? Mathf.Clamp(cx, rMinX + halfX, rMaxX - halfX) : (rMinX + rMaxX) * 0.5f; cz = (rMaxZ - halfZ >= rMinZ + halfZ) ? Mathf.Clamp(cz, rMinZ + halfZ, rMaxZ - halfZ) : (rMinZ + rMaxZ) * 0.5f; center = new Vector3(cx, center.y, cz); } Vector3 startP = center - axis * (islandLen * 0.5f); Vector3 endP = center + axis * (islandLen * 0.5f); if (doubleRow) yield return PlaceIslandDoubleRow("floor", startP, endP, mur4, islandCabs, sinkInIsland); else yield return PlaceIslandBaseRow("floor", startP, endP, u, islandCabs, sinkInIsland); } else { AiIslandFail($"Espace insuffisant (libre {freeLen:0}x{freeDepth:0}\") : îlot non généré."); } } else { AiIslandFail("Aucun espace libre respectant le dégagement de 36\" : îlot non généré."); } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } static void AiIslandFail(string reason) { Debug.LogWarning("[Island] " + reason); } // Largeur (Header.A) d'un caisson, 0 si l'id est vide ou absent de la librairie du magasin. static float SafeCabinetWidth(string id) { if (string.IsNullOrEmpty(id)) return 0f; try { return DOIT.ConvertStringToNumber(UIT.Value(Library.Cabinet, id, Header.A)); } catch { return 0f; } } // Largeurs de caissons de base réellement disponibles chez ce client, de la plus large à la plus // petite. Repli sur _AI.BASE si aucun _AI.BaseXX n'est renseigné. static List BaseWidths() { var widths = new List(); foreach (string id in new[] { _AI.Base36, _AI.Base33, _AI.Base30, _AI.Base27, _AI.Base24, _AI.Base21, _AI.Base18, _AI.Base15, _AI.Base12, _AI.Base9 }) { float w = SafeCabinetWidth(id); if (w > 0f) widths.Add(w); } if (widths.Count == 0) { float bw = SafeCabinetWidth(_AI.BASE); if (bw > 0f) widths.Add(bw); } widths.Sort((a, b) => b.CompareTo(a)); return widths; } // Ce qui restera VIDE après avoir rempli 'length' de caissons de base (même remplissage glouton, // plus large d'abord, que la rangée réelle). C'est l'espace qui finira en filler. static float BaseFillLeftover(float length) { if (length <= 0f) return 0f; List widths = BaseWidths(); if (widths.Count == 0) return length; float remaining = length; bool added = true; while (added) { added = false; foreach (float w in widths) if (w <= remaining + 0.01f) { remaining -= w; added = true; break; } } return remaining; } // Caisson de coin à poser dans un angle à 90° : le PLUS GRAND qui tient sur les deux murs, à // condition de ne pas laisser plus de vide qu'un plus petit. Un coin plus large réduit d'autant // la longueur restante de chaque mur : si ce reste n'est plus comblable par les caissons B1 // disponibles, il finit en filler — un coin plus étroit remplit alors mieux. "" = aucun ne tient. static string ChooseCornerBaseModel(float spaceWall, float spacePrevious) { string best = ""; float bestW = 0f; float bestWaste = float.MaxValue; foreach (string id in new[] { _AI.CB9036, _AI.CB90 }) { // UIT.ValueFloat et NON SafeCabinetWidth : la largeur d'un caisson de coin peut être // notée "a_b" dans la librairie, et seul ValueFloat sait en extraire la bonne valeur. // Avec ConvertStringToNumber on lisait 0 -> aucun coin ne "tenait" et la rangée de // caissons B1 venait occuper l'angle. float w = string.IsNullOrEmpty(id) ? 0f : UIT.ValueFloat(Library.Cabinet, id, Header.A); if (w <= 0f) continue; if (spaceWall < w || spacePrevious < w) continue; // ne tient pas dans ce coin float waste = BaseFillLeftover(spaceWall - w) + BaseFillLeftover(spacePrevious - w); print("[AI] coin candidat " + id + " (" + w + "\") -> perte " + waste + "\""); // À perte égale (0.5" près) on garde le plus grand caisson de coin. if (waste < bestWaste - 0.5f || (waste <= bestWaste + 0.5f && w > bestW)) { best = id; bestW = w; bestWaste = waste; } } print("[AI] coin retenu = " + (string.IsNullOrEmpty(best) ? "aucun" : best)); return best; } // Y a-t-il déjà un caisson évier posé CONTRE UN MUR ? Sinon l'îlot doit accueillir l'évier. static bool SinkOnWallExists() { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (string.IsNullOrEmpty(o[22]) || o[22] == "floor") continue; // "floor" = îlot, pas un mur if (o[4] == "sink" || o[1] == _AI.BASESINK) return true; } return false; } // Compose la longueur de l'îlot avec des caissons de largeurs variées (36,33,...,9), // pour remplir au mieux 'targetLen' sans jamais le dépasser (glouton : plus large d'abord). // reserveSink : réserve d'abord un caisson évier (aucun évier au mur -> il va dans l'îlot). static List<(string id, float w)> BuildIslandCabinets(float targetLen, bool reserveSink) { string[] ids = { _AI.Base36, _AI.Base33, _AI.Base30, _AI.Base27, _AI.Base24, _AI.Base21, _AI.Base18, _AI.Base15, _AI.Base12, _AI.Base9 }; var avail = new List<(string id, float w)>(); foreach (string id in ids) { float w = SafeCabinetWidth(id); if (w > 0f) avail.Add((id, w)); } if (avail.Count == 0) // repli si _AI.BaseXX non renseignés / absents de la librairie { float bw = Mathf.Max(1f, SafeCabinetWidth(_AI.BASE)); avail.Add((_AI.BASE, bw)); } avail.Sort((a, b) => b.w.CompareTo(a.w)); // du plus large au plus petit var result = new List<(string id, float w)>(); float remaining = targetLen; // Réserver le caisson ÉVIER en premier : sans évier au mur, c'est l'îlot qui le porte, // il est donc prioritaire sur le reste du remplissage. int sinkIndex = -1; if (reserveSink) { float wSink = SafeCabinetWidth(_AI.BASESINK); if (wSink > 0f && wSink <= remaining + 0.01f) { sinkIndex = result.Count; result.Add((_AI.BASESINK, wSink)); remaining -= wSink; } } // Réserver un caisson 24" pour qu'un lave-vaisselle puisse toujours le remplacer (AddDishwasher). float w24 = SafeCabinetWidth(_AI.Base24); if (w24 > 0f && w24 <= remaining + 0.01f) { result.Add((_AI.Base24, w24)); remaining -= w24; } // Remplissage glouton du reste (plus large d'abord). bool added = true; while (added) { added = false; foreach (var cab in avail) { if (cab.w <= remaining + 0.01f) { result.Add(cab); remaining -= cab.w; added = true; break; } } } // Recentrer l'évier dans la rangée : réservé en premier, il se retrouverait sinon collé // à l'extrémité de l'îlot. if (sinkIndex >= 0 && result.Count > 1) { var sink = result[sinkIndex]; result.RemoveAt(sinkIndex); result.Insert(result.Count / 2, sink); } return result; } // Objets du mur qui font partie du PLAN DE TRAVAIL : caissons de base/tall, four, frigo, // lave-vaisselle. Étendue [début,fin] en distance le long du mur. Les caissons muraux hauts // sont exclus (ils ne définissent pas la ligne de comptoir). static List<(float s, float e)> CounterRunItems(string wallM, Vector3 start, Vector3 dir) { var list = new List<(float s, float e)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (string.IsNullOrEmpty(o[22]) || o[22].Replace("w", "m") != wallM) continue; if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (o[2] != "Cabinet" && n4 != "stov" && n4 != "frid" && n4 != "dish") continue; GameObject g = Get.o2("SCENE", o[0]); if (g == null) continue; // Coins PBLB/PBRB quand ils existent ; sinon centre ± demi-largeur (appareils). Transform L = g.transform.Find("PBLB"); Transform R = g.transform.Find("PBRB"); float d0, d1; if (L != null && R != null) { d0 = Vector3.Dot(L.position - start, dir); d1 = Vector3.Dot(R.position - start, dir); } else { float c = Vector3.Dot(g.transform.position - start, dir); float half = AiGet.ObjectWide(g) * 0.5f; d0 = c - half; d1 = c + half; } list.Add((Mathf.Min(d0, d1), Mathf.Max(d0, d1))); } return list; } // Étendue MONDE, le long de l'axe LONGUEUR, de la TRAVÉE DE COMPTOIR d'un mur = sections // autorisées (les caissons), PROLONGÉES par les appareils qui les touchent. ComputeWallSections // traite four et frigo comme des trous : sans ce prolongement, un appareil en bout de mur // amputerait la zone d'îlot de sa largeur alors qu'il fait partie du plan de travail. // Un objet séparé par un vrai trou (patio, porte, mur nu) ne prolonge rien. static bool CounterRunSpanAlongAxis(string wall, bool lengthAlongX, out float spanMin, out float spanMax) { spanMin = float.PositiveInfinity; spanMax = float.NegativeInfinity; string w = string.IsNullOrEmpty(wall) ? "" : wall.Replace("w", "m"); if (!_sections.TryGetValue(w, out var secs) || secs == null || secs.Count == 0) return false; HashSet allowed = WallSections.TryGetValue(w, out var a) ? a : null; Vector3 start = AiGet.WallStartPoint(w); Vector3 dir = (AiGet.WallEndPoint(w) - start).normalized; // 1) Travée de départ = sections autorisées, en distance le long du mur. float dMin = float.PositiveInfinity, dMax = float.NegativeInfinity; for (int i = 0; i < secs.Count; i++) { if (allowed != null && !allowed.Contains(i + 1)) continue; // section non retenue par l'utilisateur if (secs[i].End - secs[i].Start < 0.5f) continue; // section vide dMin = Mathf.Min(dMin, secs[i].Start); dMax = Mathf.Max(dMax, secs[i].End); } if (dMax <= dMin) return false; // 2) Prolonger tant qu'un élément du plan de travail touche la travée (tolérance 1"). // Itératif : un frigo collé à un four collé à une section étend bien les deux. var items = CounterRunItems(w, start, dir); bool grown = true; while (grown) { grown = false; foreach (var (s, e) in items) { if (e < dMin - 1f || s > dMax + 1f) continue; // séparé par un trou -> ne prolonge pas if (s < dMin) { dMin = s; grown = true; } if (e > dMax) { dMax = e; grown = true; } } } Vector3 p0 = start + dir * dMin; Vector3 p1 = start + dir * dMax; float a0 = lengthAlongX ? p0.x : p0.z; float a1 = lengthAlongX ? p1.x : p1.z; spanMin = Mathf.Min(a0, a1); spanMax = Mathf.Max(a0, a1); return spanMax > spanMin; } // Clippe la zone (bornes monde) à l'étendue des runs de caissons = "lignes d'ajustement" : // - longueur limitée aux SECTIONS du mur 4 ET au run de caissons (le plus serré des deux), // - profondeur limitée au run du/des mur(s) perpendiculaire(s) (le long de l'axe profondeur). static void ClipZoneToCabinetRuns(string mur4, bool lengthAlongX, ref float rMinX, ref float rMaxX, ref float rMinZ, ref float rMaxZ) { float mur4Min = float.PositiveInfinity, mur4Max = float.NegativeInfinity; // LONGUEUR (run mur 4) float perpMin = float.PositiveInfinity, perpMax = float.NegativeInfinity; // PROFONDEUR (murs perpendiculaires) float mur4DepSum = 0f; int mur4DepCount = 0; // profondeur du mur 4 (côté proche) float oppLow = float.PositiveInfinity, oppHigh = float.NegativeInfinity; // fronts du mur parallèle OPPOSÉ (frigo, etc.) for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; string wall = o[22]; if (string.IsNullOrEmpty(wall) || wall == "floor") continue; if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; // caissons muraux hauts string c4 = o[4]; if (c4 == "wind" || c4 == "door" || c4 == "open" || c4 == "pati") continue; if (c4 == "B10" || c4 == "W10") continue; // coins morts : à la jonction, ne définissent pas le run float cx = DOIT.ConvertStringToNumber(o[15]); float cz = DOIT.ConvertStringToNumber(o[17]); float w = DOIT.ConvertStringToNumber(o[6]); float d = DOIT.ConvertStringToNumber(o[8]); if (w <= 0f || d <= 0f) continue; float ang = DOIT.ConvertStringToNumber(o[19]) * Mathf.Deg2Rad; float exX = Mathf.Abs(Mathf.Cos(ang)) * w * 0.5f + Mathf.Abs(Mathf.Sin(ang)) * d * 0.5f; float exZ = Mathf.Abs(Mathf.Sin(ang)) * w * 0.5f + Mathf.Abs(Mathf.Cos(ang)) * d * 0.5f; float projLen = lengthAlongX ? cx : cz; float exLen = lengthAlongX ? exX : exZ; float projDep = lengthAlongX ? cz : cx; float exDep = lengthAlongX ? exZ : exX; if (wall == mur4) { mur4Min = Mathf.Min(mur4Min, projLen - exLen); mur4Max = Mathf.Max(mur4Max, projLen + exLen); mur4DepSum += projDep; mur4DepCount++; } else if (WallAlongX(wall) != lengthAlongX) { // Mur perpendiculaire -> borne la PROFONDEUR (le "coin"). perpMin = Mathf.Min(perpMin, projDep - exDep); perpMax = Mathf.Max(perpMax, projDep + exDep); } else { // Mur PARALLÈLE opposé (frigo, caissons, panneau en face) -> bornera le côté profond opposé. oppLow = Mathf.Min(oppLow, projDep - exDep); oppHigh = Mathf.Max(oppHigh, projDep + exDep); } } // Longueur : clipper à la TRAVÉE DE COMPTOIR du mur 4 — sections porteuses de caissons, // prolongées par le four / le frigo / le lave-vaisselle qui les touchent. C'est la référence : // l'îlot ne doit jamais dépasser latéralement le plan de travail (au-delà : patio, porte, // mur nu). Le run brut de caissons ne sert plus qu'à resserrer, jamais à élargir. if (CounterRunSpanAlongAxis(mur4, lengthAlongX, out float runMin, out float runMax)) { if (lengthAlongX) { rMinX = Mathf.Max(rMinX, runMin); rMaxX = Mathf.Min(rMaxX, runMax); } else { rMinZ = Mathf.Max(rMinZ, runMin); rMaxZ = Mathf.Min(rMaxZ, runMax); } } // Resserrage : emprise réelle des objets posés sur le mur 4 (appareils compris). Utile quand // une section autorisée n'a pas été remplie — elle ne doit pas élargir la zone pour rien. if (mur4Max > mur4Min) { if (lengthAlongX) { rMinX = Mathf.Max(rMinX, mur4Min); rMaxX = Mathf.Min(rMaxX, mur4Max); } else { rMinZ = Mathf.Max(rMinZ, mur4Min); rMaxZ = Mathf.Min(rMaxZ, mur4Max); } } // Profondeur : clipper au run perpendiculaire. if (perpMax > perpMin) { if (lengthAlongX) { rMinZ = Mathf.Max(rMinZ, perpMin); rMaxZ = Mathf.Min(rMaxZ, perpMax); } else { rMinX = Mathf.Max(rMinX, perpMin); rMaxX = Mathf.Min(rMaxX, perpMax); } } // Profondeur : clipper aussi au front du mur PARALLÈLE opposé (frigo/caissons/panneau en face), // du côté opposé au mur 4. if (mur4DepCount > 0 && oppHigh > oppLow) { float mur4Dep = mur4DepSum / mur4DepCount; float oppCenter = (oppLow + oppHigh) * 0.5f; if (mur4Dep < oppCenter) { // Mur opposé du côté PROFOND (grand) -> borne le max à son front (oppLow). if (lengthAlongX) rMaxZ = Mathf.Min(rMaxZ, oppLow); else rMaxX = Mathf.Min(rMaxX, oppLow); } else { // Mur opposé du côté PROCHE (petit) -> borne le min à son front (oppHigh). if (lengthAlongX) rMinZ = Mathf.Max(rMinZ, oppHigh); else rMinX = Mathf.Max(rMinX, oppHigh); } } } // Trace la zone îlot disponible : rectangle rouge (LineRenderer, épaisseur 2) à la hauteur y. static GameObject _islandZoneViz; static void DrawIslandZone(float minX, float maxX, float minZ, float maxZ, float y) { if (_islandZoneViz == null) { _islandZoneViz = new GameObject("IslandZoneViz"); var lr0 = _islandZoneViz.AddComponent(); // Shader compatible URP (repli si absent). Shader sh = Shader.Find("Universal Render Pipeline/Unlit"); if (sh == null) sh = Shader.Find("Sprites/Default"); var mat = new Material(sh); if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.red); if (mat.HasProperty("_Color")) mat.SetColor("_Color", Color.red); lr0.material = mat; lr0.startColor = lr0.endColor = Color.red; lr0.startWidth = lr0.endWidth = 2f; lr0.numCornerVertices = 2; lr0.loop = true; lr0.useWorldSpace = true; lr0.positionCount = 4; } var lr = _islandZoneViz.GetComponent(); lr.SetPosition(0, new Vector3(minX, y, minZ)); lr.SetPosition(1, new Vector3(maxX, y, minZ)); lr.SetPosition(2, new Vector3(maxX, y, maxZ)); lr.SetPosition(3, new Vector3(minX, y, maxZ)); } // Mur portant le plus fort poids fonctionnel : évier=3, cuisson=3, frigo=2, armoire(prépa)=1. static string ChooseFunctionalWall() { var weights = new Dictionary(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; string wall = o[22]; if (string.IsNullOrEmpty(wall) || wall == "floor") continue; int wgt = o[4] switch { "sink" => 3, "stov" => 3, "frid" => 2, _ => 0 }; if (wgt == 0 && o[2] == "Cabinet") wgt = 1; if (wgt == 0) continue; weights.TryGetValue(wall, out int cur); weights[wall] = cur + wgt; } string best = null; int bestW = 0; foreach (var kv in weights) if (kv.Value > bestW) { bestW = kv.Value; best = kv.Key; } return best; // peut être null (aucun appareil/armoire) } // Vrai si le mur court le long de l'axe X (mur horizontal) -> l'îlot qui lui fait face a sa longueur en X. static bool WallAlongX(string wall) { GameObject go = string.IsNullOrEmpty(wall) ? null : Get.o1(wall); if (go != null) { Vector3 rt = go.transform.right; return Mathf.Abs(rt.x) >= Mathf.Abs(rt.z); } return true; } // Longueur DISPONIBLE d'un mur = somme de ses sections VALIDES (modèle de sections), en pouces. // Plus fiable que la somme des caissons placés : les coins morts, petits caissons de remplissage // et appareils faussent l'étendue réelle du run. Mur en format "m3". static float ValidSectionsLength(string wallM) { if (string.IsNullOrEmpty(wallM) || !_sections.TryGetValue(wallM, out var secs) || secs == null) return 0f; HashSet allowed = WallSections.TryGetValue(wallM, out var a) ? a : null; float total = 0f; for (int i = 0; i < secs.Count; i++) { if (allowed != null && !allowed.Contains(i + 1)) continue; // section non autorisée total += Mathf.Max(0f, secs[i].End - secs[i].Start); } return total; } // Mur à faire face (mur 4) : celui dont la longueur DISPONIBLE (total des sections valides) est la // plus grande, pour que le devant de l'îlot (portes) soit parallèle au plus long alignement possible // de caissons. Égalité -> mur de la cuisinière. Basé sur les sections, pas sur les caissons placés. static string ChooseIslandWall(string funcWall) { string longestWall = null; // format "m3" float maxLen = -1f; foreach (var kv in _sections) { float len = ValidSectionsLength(kv.Key); if (len > maxLen) { maxLen = len; longestWall = kv.Key; } } if (longestWall == null) return funcWall; // pas de sections calculées -> repli fonctionnel // Mur portant la cuisinière (départage prioritaire), en format "m3". string stoveWallM = null; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[4] == "stov" && !string.IsNullOrEmpty(o[22]) && o[22] != "floor") { stoveWallM = o[22].Replace("w", "m"); break; } } // Règle : si le mur de la cuisinière a une longueur dispo >= 80% du plus long, on le choisit. float stoveLen = ValidSectionsLength(stoveWallM); string best = (stoveWallM != null && stoveLen >= 0.8f * maxLen) ? stoveWallM : longestWall; return best.Replace("m", "w"); // format o[22] attendu par le reste du code } // Construit la carte d'occupation du sol (grille booléenne). Une cellule est "occupée" si elle // est trop proche d'un mur (dégagement) ou dans l'emprise (tampon de dégagement) d'une armoire/appareil. // Les cellules libres restantes garantissent donc déjà le dégagement mini par construction. // reserveCabinetDepth : ajoute la PROFONDEUR d'un caisson de base au dégagement des murs qui vont // porter des caissons. À utiliser quand la grille est construite AVANT la pose des caissons — leurs // emprises (étape 2) ne sont pas encore là pour produire ce retrait, et le rectangle libre sortirait // trop grand d'environ 24" par mur. // Ouverture murale (fenêtre, porte, passage, patio) : ne repose pas sur le sol, donc // n'obstrue rien pour un meuble. Le test se faisait sur o[4] comparé à des minuscules — // or CreateOpen écrit "Open", CreateDoor "Door", et le patio ne renseigne pas o[4] du tout : // SEULES les fenêtres étaient ignorées. Portes, passages et patios comptaient comme des // meubles au sol, ce qui interdisait toute péninsule dont le comptoir longe une ouverture. // On lit donc la CATÉGORIE o[2], qui est la source fiable (celle qu'utilise // ComputeWallSections), avec o[4] en secours et sans distinction de casse. static bool IsWallOpening(string[] o) { if (o == null) return false; string cat = o.Length > 2 ? o[2] : ""; if (cat == "Window" || cat == "Door" || cat == "Opening" || cat == "Patio") return true; string c4 = (o.Length > 4 ? o[4] : "") ?? ""; if (c4.Length > 4) c4 = c4[..4]; c4 = c4.ToLower(); return c4 == "wind" || c4 == "door" || c4 == "open" || c4 == "pati"; } // wallClear / objectBuffer : -1 => valeurs d'ilot (36" au mur, 36/42" autour des objets). // La PENINSULE demande une carte DURE (0 et 0) : elle est adossee au mur et verifie elle-meme // ses allees la ou elles ont un sens. // ZONE DISPONIBLE d'une peninsule : on part de l'aire de la piece et on retranche // - une bande de PENINSULA_ZONE_BARE_WALL le long de chaque mur SANS caissons ; // - une bande de PENINSULA_ZONE_CAB_WALL le long de chaque mur AVEC caissons, mais seulement // sur la distance couverte par ses objets, du premier au dernier. // Les 72" ne sont pas arbitraires : 24" de caisson plus 48" d'allee. C'est la meme regle que // les degagements que j'empilais bande par bande, mais posee UNE fois sur toute la piece — d'ou // le fait que les bandes locales disparaissent en aval, sinon elles compteraient deux fois. const float PENINSULA_ZONE_BARE_WALL = 48f; const float PENINSULA_ZONE_CAB_WALL = 72f; // Marge tolerée sur la projection le long du mur : l'emprise des objets est prise sur leur // demi-largeur, et la grille echantillonne au pas de 2". const float PENINSULA_ZONE_SPAN_PAD = 0f; // Objets qui BORNENT la portion occupee d'un mur, pour le calcul de la zone. // La zone se determine AVANT la rangee basse : compter les caissons bas la ferait dependre de // ce qu'on cherche justement a placer, et elle changerait a chaque passe. // Ne comptent donc que les reperes fixes du design : caissons de COIN, TALL, caisson EVIER, // CUISINIERE et REFRIGERATEUR. static bool ZoneAnchorObject(GameObject o) { if (o == null) return false; int i = Get.GetObjectIndex(o.name); if (i < 0 || !DOIT.exist(i)) return false; string[] a = _G.OBJs[i]; if (a == null || a.Length < 5) return false; string name = a[0]; string uid = a[1]; string model = a[4]; // Caissons de COIN bas (B2..B5 pour les coins droits, B9/B10 pour les coins morts). if (model == "B2" || model == "B3" || model == "B4" || model == "B5" || model == "B9" || model == "B10") return true; if (model == "T1") return true; // TALL / garde-manger if (!string.IsNullOrEmpty(uid) && uid == _AI.BASESINK) return true; // caisson EVIER // CUISINIERE et REFRIGERATEUR seulement : les autres appareils — lave-vaisselle, // micro-ondes — arrivent apres et ne doivent pas deplacer la zone d'une passe a l'autre. if (name.Length >= 4) { string p4 = name.Substring(0, 4); if (p4 == "stov" || p4 == "frid") return true; } return false; } // Portion d'un mur que la ZONE vient toucher : c'est la seule ou une peninsule peut s'adosser, // et ce mur devient le COTE MUR. Rend false si la zone ne touche ce mur nulle part. static bool ZoneTouchSpan(bool[,] occ, int cols, int rows, float ox, float oz, Vector3 wStart, Vector3 dir, Vector3 forward, float wallLen, float baseDepth, out float u0, out float u1) { u0 = float.MaxValue; u1 = float.MinValue; // On sonde a mi-profondeur d'un caisson : "la zone touche le mur ici" veut dire qu'un // caisson adosse y tiendrait. float probe = baseDepth * 0.5f; for (float u = 0f; u <= wallLen + 0.01f; u += ISLAND_GRID_STEP) { Vector3 pt = wStart + dir * u + forward * probe; if (!RectFree(occ, cols, rows, ox, oz, pt, forward, ISLAND_GRID_STEP, dir, ISLAND_GRID_STEP, out _)) continue; u0 = Mathf.Min(u0, u); u1 = Mathf.Max(u1, u); } return u1 > u0; } static void BuildPeninsulaZone(out bool[,] occ, out int cols, out int rows, out float ox, out float oz) { // Carte DURE : hors piece + emprises reelles des objets, sans aucun inset de mur. BuildOccupancyGrid(out occ, out cols, out rows, out ox, out oz, false, 0f, 0f); GetRoomBoundary(out List poly, out List segA, out List segB, out List segWall); if (segA.Count == 0) return; // Pour chaque mur : porte-t-il des objets, et sur quelle portion ? int n = segA.Count; var bandDist = new float[n]; var spanLo = new float[n]; var spanHi = new float[n]; for (int k = 0; k < n; k++) { string wallM = segWall[k].Replace("w", "m"); Vector2 a = segA[k], b = segB[k]; Vector2 dir2 = (b - a).normalized; float wallLen = Vector2.Distance(a, b); List obs = AiGet.AllObjectsOnWall(wallM, "All", "All", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine); float lo = float.MaxValue, hi = float.MinValue; if (obs != null) { foreach (GameObject o in obs) { if (!ZoneAnchorObject(o)) continue; // ni caissons bas ni petits appareils Vector2 c = new(o.transform.position.x, o.transform.position.z); float u = Vector2.Dot(c - a, dir2); float half = AiGet.ObjectWide(o) * 0.5f; lo = Mathf.Min(lo, u - half); hi = Mathf.Max(hi, u + half); } } if (hi > lo) { // Mur AVEC caissons : 72" sur la seule portion couverte par ses objets. bandDist[k] = PENINSULA_ZONE_CAB_WALL; spanLo[k] = lo - PENINSULA_ZONE_SPAN_PAD; spanHi[k] = hi + PENINSULA_ZONE_SPAN_PAD; } else { // Mur SANS caissons : 48" sur toute sa longueur. bandDist[k] = PENINSULA_ZONE_BARE_WALL; spanLo[k] = 0f; spanHi[k] = wallLen; } print("[AI] Zone peninsule : " + wallM + (hi > lo ? " AVEC" : " SANS") + " caissons -> bande de " + bandDist[k] + " sur " + spanLo[k].ToString("0.0") + "-" + spanHi[k].ToString("0.0") + " (mur de " + wallLen.ToString("0.0") + ")."); } float step = ISLAND_GRID_STEP; for (int r = 0; r < rows; r++) { float z = oz + (r + 0.5f) * step; for (int c = 0; c < cols; c++) { if (occ[r, c]) continue; Vector2 pt = new(ox + (c + 0.5f) * step, z); for (int k = 0; k < n; k++) { if (DistPointSeg(pt, segA[k], segB[k]) >= bandDist[k]) continue; Vector2 dir2 = (segB[k] - segA[k]).normalized; float u = Vector2.Dot(pt - segA[k], dir2); if (u < spanLo[k] || u > spanHi[k]) continue; occ[r, c] = true; break; } } } } static void BuildOccupancyGrid(out bool[,] occ, out int cols, out int rows, out float originX, out float originZ, bool reserveCabinetDepth = false, float wallClear = -1f, float objectBuffer = -1f) { float roomClear = wallClear >= 0f ? wallClear : ISLAND_MIN_CLEAR; float step = ISLAND_GRID_STEP; // 1) Frontière de la pièce (polygone des murs) : gère aussi les pièces en L. // Cellule occupée si HORS de la pièce, ou à moins du dégagement du mur. // Dégagement par mur : 60" (24 caisson + 36) si le mur porte des caissons, 36" sinon. GetRoomBoundary(out List poly, out List segA, out List segB, out List segWall); bool hasPoly = poly.Count >= 3; // ÉTENDUE de la grille : le rectangle _G.WIDE x _G.DEPTH est un RÉGLAGE de pièce, il ne // couvre pas forcément l'emprise réelle des murs (pièce en L, mur déplacé, profondeur de // pièce différente de l'écart entre les murs). Or tout échantillon tombant hors grille est // compté comme OCCUPÉ : un meuble adossé à un mur situé au-delà de la grille était donc // rejeté d'office. L'îlot, au milieu de la pièce, ne rencontrait jamais le cas ; la // péninsule, adossée au mur, le rencontre systématiquement. float gMinX = -_G.WIDE * 0.5f, gMaxX = _G.WIDE * 0.5f; float gMinZ = -_G.DEPTH * 0.5f, gMaxZ = _G.DEPTH * 0.5f; if (hasPoly) { foreach (Vector2 v in poly) { gMinX = Mathf.Min(gMinX, v.x); gMaxX = Mathf.Max(gMaxX, v.x); gMinZ = Mathf.Min(gMinZ, v.y); gMaxZ = Mathf.Max(gMaxZ, v.y); } } originX = gMinX; originZ = gMinZ; cols = Mathf.Max(1, Mathf.CeilToInt((gMaxX - gMinX) / step)); rows = Mathf.Max(1, Mathf.CeilToInt((gMaxZ - gMinZ) / step)); occ = new bool[rows, cols]; // Inset des murs = 36" partout. Le 60" (24 caisson + 36) là où il y a des caissons est produit // par les EMPRISES des caissons (étape 2), qui s'arrêtent au bout du run = la "ligne d'ajustement". float[] segClear = new float[segWall.Count]; float cabDepth = _AI.BaseDepth + _AI.Door1Thickness; for (int k = 0; k < segWall.Count; k++) { segClear[k] = roomClear; if (reserveCabinetDepth && WallSelected != null && WallSelected.Contains(segWall[k].Replace("w", "m"))) segClear[k] += cabDepth; // caissons pas encore posés : on réserve leur profondeur } // Repli rectangle si la frontière n'est pas exploitable float minX = gMinX + roomClear; float maxX = gMaxX - roomClear; float minZ = gMinZ + roomClear; float maxZ = gMaxZ - roomClear; for (int r = 0; r < rows; r++) { float z = originZ + (r + 0.5f) * step; for (int c = 0; c < cols; c++) { float x = originX + (c + 0.5f) * step; bool occupied; if (hasPoly) { Vector2 p = new(x, z); occupied = !PointInPolygon(p, poly); if (!occupied) { for (int k = 0; k < segA.Count; k++) if (DistPointSeg(p, segA[k], segB[k]) < segClear[k]) { occupied = true; break; } } } else { occupied = x < minX || x > maxX || z < minZ || z > maxZ; } if (occupied) occ[r, c] = true; } } // 2) Emprises des objets au sol + tampon de dégagement for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; string cat4 = o[4]; if (IsWallOpening(o)) continue; // ouvertures murales : rien au sol if (o[0].Length >= 4) { string n4 = o[0][..4]; if (n4 == "lamp" || n4 == "stoo" || n4 == "hood") continue; // n'obstrue pas le sol } if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; // armoire murale (en hauteur) float cx = DOIT.ConvertStringToNumber(o[15]); float cz = DOIT.ConvertStringToNumber(o[17]); float w = DOIT.ConvertStringToNumber(o[6]); float d = DOIT.ConvertStringToNumber(o[8]); if (w <= 0f || d <= 0f) continue; float ang = DOIT.ConvertStringToNumber(o[19]) * Mathf.Deg2Rad; bool appliance = cat4 == "stov" || cat4 == "frid" || cat4 == "sink" || cat4 == "dish"; float buffer = objectBuffer >= 0f ? objectBuffer : (appliance ? ISLAND_APPLIANCE_CLEAR : ISLAND_MIN_CLEAR); // AABB (boîte alignée) d'un rectangle tourné, élargie du dégagement float exX = Mathf.Abs(Mathf.Cos(ang)) * w * 0.5f + Mathf.Abs(Mathf.Sin(ang)) * d * 0.5f + buffer; float exZ = Mathf.Abs(Mathf.Sin(ang)) * w * 0.5f + Mathf.Abs(Mathf.Cos(ang)) * d * 0.5f + buffer; // Une cellule est occupée quand son CENTRE tombe dans l'emprise — même règle qu'à // l'étape 1 (polygone et murs). Avant, Floor/Ceil marquaient toute cellule seulement // EFFLEURÉE : chaque objet grossissait de près d'une cellule (2") dans les quatre // directions. Invisible pour l'îlot, dont les tampons font 36/42", mais fatal pour la // péninsule, testée sans tampon : le premier pouce de la zone remplissable — qui // commence exactement là où finit l'obstacle précédent — était marqué occupé, et aucun // emplacement adossé au mur ne pouvait passer. // Centre de la cellule c = originX + (c + 0.5) * step. int cMin = Mathf.Max(0, Mathf.CeilToInt((cx - exX - originX) / step - 0.5f)); int cMax = Mathf.Min(cols - 1, Mathf.FloorToInt((cx + exX - originX) / step - 0.5f)); int rMin = Mathf.Max(0, Mathf.CeilToInt((cz - exZ - originZ) / step - 0.5f)); int rMax = Mathf.Min(rows - 1, Mathf.FloorToInt((cz + exZ - originZ) / step - 0.5f)); for (int r = rMin; r <= rMax; r++) for (int c = cMin; c <= cMax; c++) occ[r, c] = true; } } // Frontière de la pièce à partir des murs (XZ) : polygone (starts) + segments (start/end) + nom du mur. static void GetRoomBoundary(out List poly, out List segA, out List segB, out List segWall) { poly = new(); segA = new(); segB = new(); segWall = new(); var ewp = _G.ExteriorWallPoints; if (ewp == null) return; for (int i = 0; i < ewp.Length; i++) { if (ewp[i] == null || ewp[i].Count < 2) continue; Vector3 s = ewp[i][0]; Vector3 e = ewp[i][1]; poly.Add(new Vector2(s.x, s.z)); segA.Add(new Vector2(s.x, s.z)); segB.Add(new Vector2(e.x, e.z)); segWall.Add("w" + (i + 1)); // ExteriorWallPoints[i] <-> mur "w{i+1}" } } // Point dans polygone (lancer de rayon). static bool PointInPolygon(Vector2 p, List poly) { bool inside = false; int n = poly.Count; for (int i = 0, j = n - 1; i < n; j = i++) { Vector2 a = poly[i]; Vector2 b = poly[j]; if (((a.y > p.y) != (b.y > p.y)) && (p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y + 1e-6f) + a.x)) inside = !inside; } return inside; } // Distance d'un point au segment [a,b]. static float DistPointSeg(Vector2 p, Vector2 a, Vector2 b) { Vector2 ab = b - a; float len2 = ab.sqrMagnitude; float t = len2 > 1e-6f ? Mathf.Clamp01(Vector2.Dot(p - a, ab) / len2) : 0f; return Vector2.Distance(p, a + t * ab); } // Plus grand rectangle de cellules libres (false) dans la grille (histogramme + pile, O(rows*cols)). static bool LargestFreeRectangle(bool[,] occ, int cols, int rows, out int bestC, out int bestR, out int bestW, out int bestH) { bestC = bestR = bestW = bestH = 0; int bestArea = 0; int[] heights = new int[cols]; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) heights[c] = occ[r, c] ? 0 : heights[c] + 1; var st = new Stack(); for (int c = 0; c <= cols; c++) { int curr = (c == cols) ? 0 : heights[c]; while (st.Count > 0 && heights[st.Peek()] >= curr) { int h = heights[st.Pop()]; int left = st.Count == 0 ? 0 : st.Peek() + 1; int width = c - left; int area = h * width; if (h > 0 && width > 0 && area > bestArea) { bestArea = area; bestW = width; bestH = h; bestC = left; bestR = r - h + 1; } } st.Push(c); } } return bestArea > 0; } // Pose une rangée d'armoires de base (largeur standard) centrée sur [start,end], projections aux extrémités. // reversed : la rangée est tournée de 180° (rangée arrière d'un double îlot) -> on inverse les projections. // counterProj : chaîne SC[42] "H_left_back_right_front_..." (index 2 = arrière, 4 = avant). // equipSink : monte l'évier + robinet sur le caisson _AI.BASESINK de la rangée. En double rangée, // seule celle qui fait face au comptoir mural le reçoit (l'autre garde le caisson, sans évier, // pour que les deux rangées gardent exactement les mêmes largeurs). static IEnumerator PlaceCabRun(string WallNumber, Vector3 start, Vector3 end, float angle, bool reversed, string counterProj, List<(string id, float w)> cabs, bool equipSink = false) { Vector3 direction = (end - start).normalized; int count = cabs.Count; float pos = 0f; // distance cumulée depuis start for (int i = 0; i < count; i++) { float cw = cabs[i].w; Vector3 cabPos = start + direction * (pos + cw * 0.5f); cabPos.y = _AI.BaseHeightPosition; Set_TemporySC(cabs[i].id, WallNumber, cabPos, angle, "F"); _G.SC[42] = counterProj; // Évier d'îlot : mêmes recettes que AddSinkCabinetToWall. if (equipSink && cabs[i].id == _AI.BASESINK) { _G.SC[67] = "SINK102,32,18,F3F3F3?Glossy?none"; _G.SC[68] = "FAUC101,10,10,F3F3F3?Glossy?none"; } // Projections cascade aux extrémités extérieures ; inversées si la rangée est tournée à 180°. bool leftFall = (i == (reversed ? 0 : count - 1)); bool rightFall = (i == (reversed ? count - 1 : 0)); _G.SC[45] = leftFall ? "ON" : "OFF"; // C[45] = projection gauche _G.SC[47] = rightFall ? "ON" : "OFF"; // C[47] = projection droite _G.SC[55] = "0,0,0,0"; _G.SC[61] = _AI.Door1; _G.SC[63] = _AI.Kick1; CreateCAB.AddCab(); pos += cw; yield return null; } } // Panneau décoratif (0.5" d'épaisseur) le long de [center ± length/2] à l'offset 'depthOffset' sur 'forward'. static void AddIslandPanel(string WallNumber, Vector3 rowCenter, Vector3 forward, float length, float angle, float depthOffset) { Vector3 size = new(0.5f, 35.5f, length); Vector3 panelPos = rowCenter + forward * depthOffset; panelPos.y = -_G.HEIGHT * 0.5f + 35.5f * 0.5f; Set_TemporySC_Panel(_AI.PANEL_ISLAND, WallNumber, size, panelPos, angle, "F"); _G.SC[60] = "GLOBAL3"; _G.SC[61] = UIT.Global(Header.Door, 3).Split("?")[1] + "?90"; CreatePnl.addpnl(); } // Îlot rangée simple : les PORTES font face au comptoir mural (wallDir) ; panneau/cascade côté opposé. static IEnumerator PlaceIslandBaseRow(string WallNumber, Vector3 start, Vector3 end, Vector3 wallDir, List<(string id, float w)> cabs, bool equipSink) { Vector3 direction = (end - start).normalized; Vector3 forward = new(-direction.z, 0f, direction.x); float angle = GetAngleFromXClockwise(end, start); float length = Vector3.Distance(start, end); // Par défaut les portes regardent +forward. On retourne l'îlot si le mur est de l'autre côté, // pour que la façade (portes) fasse face au comptoir mural et le panneau/cascade côté pièce. bool doorsFaceWall = Vector3.Dot(wallDir, forward) > 0f; float cabAngle = doorsFaceWall ? angle : angle + 180f; bool reversed = !doorsFaceWall; float panelSign = doorsFaceWall ? -1f : 1f; // panneau du côté opposé aux portes (loin du mur) // Débord ARRIÈRE (index 2) = assise des tabourets : nul si le design n'en demande pas. string counterProj = DOIT.ChangeCodeOnPos("1.25_0_10_0_1.75_0_0_0", 2, StoolOverhang().ToString(), "_"); yield return PlaceCabRun(WallNumber, start, end, cabAngle, reversed, counterProj, cabs, equipSink); Vector3 rowCenter = (start + end) * 0.5f; AddIslandPanel(WallNumber, rowCenter, forward, length, angle, panelSign * (_AI.BaseDepth+_AI.Door1Thickness) * 0.5f); yield return null; } // Îlot double rangée : deux rangées de bases dos-à-dos (fronts vers l'extérieur des deux côtés). // Le côté qui fait face au comptoir mural (mur fonctionnel) n'a PAS de débord de comptoir ; // seul le côté opposé (banquette) garde la projection. static IEnumerator PlaceIslandDoubleRow(string WallNumber, Vector3 start, Vector3 end, string funcWall, List<(string id, float w)> cabs, bool equipSink) { Vector3 direction = (end - start).normalized; Vector3 forward = new(-direction.z, 0f, direction.x); float angle = GetAngleFromXClockwise(end, start); float half = (_AI.BaseDepth+_AI.Door1Thickness) * 0.5f; // Quel côté regarde le comptoir mural ? Ce côté = aucun débord. bool plusForwardFacesWall = false; if (!string.IsNullOrEmpty(funcWall)) { GameObject w = Get.o1(funcWall); if (w != null) { Vector3 center = (start + end) * 0.5f; Vector3 toWall = w.transform.position - center; plusForwardFacesWall = Vector3.Dot(new Vector3(toWall.x, 0f, toWall.z), forward) > 0f; } } // "H_left_back_right_front_..." : projFront = débord côté banquette, // projWall = projection avant normale (1.25) côté comptoir mural. // Le débord banquette tombe à 0 sans tabourets ; le côté travail ne change pas. string projFront = DOIT.ChangeCodeOnPos("1.25_0_0_0_10_0_0_0", 4, StoolOverhang().ToString(), "_"); const string projWall = "1.25_0_0_0_1.25_0_0_0"; // Rangée +forward. L'évier va sur la rangée qui fait face au comptoir mural (côté travail), // jamais côté banquette. string projA = plusForwardFacesWall ? projWall : projFront; yield return PlaceCabRun(WallNumber, start + forward * half, end + forward * half, angle, false, projA, cabs, equipSink && plusForwardFacesWall); // Rangée -forward : tournée de 180° -> reversed pour remettre les projections du bon côté. string projB = plusForwardFacesWall ? projFront : projWall; yield return PlaceCabRun(WallNumber, start - forward * half, end - forward * half, angle + 180f, true, projB, cabs, equipSink && !plusForwardFacesWall); } //----------------------------------HOOD-------------------------------------------------------------------------- public void AddHood() { StartCoroutine(CaroutineAddHood()); } IEnumerator CaroutineAddHood() { print("Add Hood reach"); GameObject Stove = null; string WallNumber = ""; foreach (string Wall in WallSelected) { Stove = GetObject("stov", Wall); WallNumber = Wall.Replace("m", "w"); if (Stove != null) break; } if (Stove != null) { print("Add Hood reach and stove exist"); Vector3 PositionToInstall = Stove.transform.position; float StoveWide = AiGet.ObjectWide(Stove); float angle = GetAngleByWall(WallNumber); float HoodHeight = _G.HEIGHT - 60; PositionToInstall.y = -_G.HEIGHT * 0.5f + 60 + HoodHeight * 0.5f; Vector3 Size = new(StoveWide, HoodHeight, 24); Set_TemporySC_Hood(_AI.HOOD, WallNumber, Size, PositionToInstall, angle); _G.SC[40] = "On_500_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; CreateHOOD.AddHood(); DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; ButtonDoneToggle.NotifyDone(); } yield return null; } //------------------------------FRIDGE CABINET AND PANELS------------------------------------------------------ // Une COLONNE (modele T1) occupe-t-elle ce point du mur ? Une colonne porte deja sa propre // joue finie : lui accoler le panneau du frigo superpose deux panneaux au meme endroit. // 'tol' rattrape le jeu entre les deux meubles. static bool TallCabinetAt(string wall, Vector3 point, float tol) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 end = AiGet.WallEndPoint(wnum); Vector3 dir = new Vector3(end.x - start.x, 0f, end.z - start.z).normalized; float px = Vector3.Dot(new Vector3(point.x - start.x, 0f, point.z - start.z), dir); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[22] != wnum) continue; if (o[4] != "T1") continue; // T1 = colonne float cx = Vector3.Dot(new Vector3(DOIT.ConvertStringToNumber(o[15]) - start.x, 0f, DOIT.ConvertStringToNumber(o[17]) - start.z), dir); float half = DOIT.ConvertStringToNumber(o[6]) * 0.5f; if (px >= cx - half - tol && px <= cx + half + tol) { print("[AI] Panneau frigo : colonne " + o[0] + "/" + o[1] + " a cet endroit du mur " + wnum + " (panneau en " + px + ", colonne " + (cx - half) + " a " + (cx + half) + ")."); return true; } } return false; } public void AddFridgeCabinet() => StartCoroutine(AddFridgeCabinetCo()); private IEnumerator AddFridgeCabinetCo() { foreach (string Wall in WallSelected) { yield return StartCoroutine(AddFridgeCab(Wall)); } yield return _waitForSecondsRealtime2; DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } IEnumerator AddFridgeCab(string Wall) { GameObject Fridge = GetObject("frid", Wall); if (Fridge != null) { Vector3 PositionToInstall = Fridge.transform.position; Vector3 Forward = Fridge.transform.forward.normalized; //Vector3 Up = Fridge.transform.up.normalized; Vector3 Right = Fridge.transform.right.normalized; float CabinetHeight = UIT.ValueFloat(Library.Cabinet,_AI.FRIDGE,Header.B); float FridgeDepth = AiGet.ObjectDepth(Fridge); float FridgeWide = AiGet.ObjectWide(Fridge); float CabinetFridgeDepth=UIT.ValueFloat(Library.Cabinet,_AI.FRIDGE,Header.C); float CabinetFridgeWide=UIT.ValueFloat(Library.Cabinet,_AI.FRIDGE,Header.A); PositionToInstall += (FridgeDepth - CabinetFridgeDepth) * 0.5f * Forward; PositionToInstall.y = _G.HEIGHT*0.5f-12-CabinetHeight * 0.5f; float angle = GetAngleByWall(Wall); Set_TemporySC(_AI.FRIDGE, Wall, PositionToInstall, angle, "F"); _G.SC[40] = "Off"; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); // Les deux positions sont calculees d'abord : la seconde derivait de la premiere par // soustraction, donc sauter un panneau aurait decale l'autre. Vector3 size = new(0.75f, 84, CabinetFridgeDepth); float sideOffset = CabinetFridgeWide * 0.5f + 0.75f * 0.5f; float panelY = -_G.HEIGHT * 0.5f + 42; Vector3 rightPanel = PositionToInstall + sideOffset * Right; Vector3 leftPanel = PositionToInstall - sideOffset * Right; rightPanel.y = panelY; leftPanel.y = panelY; //Panel Right — sauf si une COLONNE occupe deja ce cote : elle a sa propre joue finie. if (TallCabinetAt(Wall, rightPanel, 3f)) print("[AI] Panneau frigo DROIT abandonne : colonne de ce cote."); else { Set_TemporySC_Panel(_AI.PANEL_FRIDGE, Wall, size, rightPanel, angle - 90, "F"); _G.SC[60] = "GLOBAL1"; _G.SC[61] = UIT.Global(Header.Door,1).Split("?")[1]+"?0"; _G.SC[60] = "GLOBAL2"; CreatePnl.addpnl(); } //Panel Left if (TallCabinetAt(Wall, leftPanel, 3f)) print("[AI] Panneau frigo GAUCHE abandonne : colonne de ce cote."); else { Set_TemporySC_Panel(_AI.PANEL_FRIDGE, Wall, size, leftPanel, angle - 90, "F"); _G.SC[60] = "GLOBAL1"; _G.SC[61] = UIT.Global(Header.Door,1).Split("?")[1]+"?0"; _G.SC[60] = "GLOBAL2"; CreatePnl.addpnl(); } } yield return null; } //------------------------------DRAWERS------------------------------------------------------------------ public void AddDrawers() { Vector3 stove = GetStovePosition(); Vector3 sink = GetSinkPosition(); var targets = new List(); // 1) Caisson de base le plus proche de la CUISINIÈRE (au mur). GameObject nearStove = FindClosestBaseCabinet(stove, true, null); if (nearStove != null) targets.Add(nearStove); // 2) Caisson de base le plus proche de l'ÉVIER (au mur), différent du précédent. GameObject nearSink = FindClosestBaseCabinet(sink, true, nearStove); if (nearSink != null) targets.Add(nearSink); // 3) Caisson de base sur l'ÎLOT en avant (côté portes). GameObject islandFront = FindIslandFrontCabinet(); if (islandFront != null) targets.Add(islandFront); print("[AI] Tiroirs : four=" + stove + " -> " + (nearStove == null ? "AUCUN" : nearStove.name) + " ; evier=" + sink + " -> " + (nearSink == null ? "AUCUN" : nearSink.name) + " ; ilot -> " + (islandFront == null ? "AUCUN" : islandFront.name)); foreach (GameObject Target in targets) { if (Target != null) { string drawerUniqueName = CabinetDrawerMatchSizeUniqueName(Target); if (drawerUniqueName == "") continue; string before = GetObjectUniqueName(Target); ChangeCabinet(Target, drawerUniqueName); // Trace du dernier maillon : le caisson tiroir est-il vraiment créé, et avec quel motif // de porte (ADN[25], celui qui dessine les faces de tiroir) ? string[] made = _G.OBJnum > 0 ? _G.OBJs[_G.OBJnum - 1] : null; print("[AI] Tiroir : " + before + " -> " + drawerUniqueName + " ; cree=" + (made == null ? "?" : made[0] + " modele=" + made[1] + " C4=" + made[4] + " C23=" + made[23] + " C25=" + made[25])); } } DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); } // Caisson de BASE (B1, pas l'évier ni un caisson mural haut) le plus proche de 'pos' ; // onWall = mur (true) ou îlot (false) ; 'exclude' pour ne pas reprendre le même. static GameObject FindClosestBaseCabinet(Vector3 pos, bool onWall, GameObject exclude) { GameObject best = null; float bestDist = float.MaxValue; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[2] != "Cabinet" || o[4] != "B1") continue; // caissons de base uniquement (exclut lave-vaisselle "Appliance"/"dish") if (o[0] != null && o[0].Length >= 4 && o[0][..4] == "dish") continue; // jamais un lave-vaisselle if (o[1] == _AI.BASESINK) continue; // pas le caisson évier if (DOIT.ConvertStringToNumber(o[21]) > 30f) continue; // pas les caissons muraux hauts bool isWall = o[22] != "floor"; if (isWall != onWall) continue; // Le caisson doit pouvoir DEVENIR un caisson à tiroirs. Sans ce filtre, un caisson de 9" // collé au four ou à l'évier gagnait le concours de proximité, n'avait aucun modèle de // tiroir à sa largeur, et bloquait le caisson de 24" juste à côté qui, lui, en a un. if (string.IsNullOrEmpty(DrawerModelForWidth(DOIT.ConvertStringToNumber(DOIT.CSs(o[6]))))) continue; GameObject go = Get.o2("SCENE", o[0]); if (go == null) continue; // entrée orpheline : objet déjà détruit if (exclude != null && go == exclude) continue; float x = DOIT.ConvertStringToNumber(o[15]); float z = DOIT.ConvertStringToNumber(o[17]); float dist = (x - pos.x) * (x - pos.x) + (z - pos.z) * (z - pos.z); if (dist < bestDist) { bestDist = dist; best = go; } } return best; } // Un caisson de base de l'îlot "en avant" : celui dont la face (portes) regarde le plus vers l'extérieur. static GameObject FindIslandFrontCabinet() { List islandCabs = GetAllObjectsOnFloor("floor", "Cabinet", "B1", _AI.FloorLine + _AI.CabinetHeight + 10, _AI.FloorLine, "From Start"); if (islandCabs == null) return null; // Jamais un lave-vaisselle : les tiroirs ne doivent pas remplacer un lave-vaisselle. // Et seulement une largeur pour laquelle le catalogue déclare un modèle à tiroirs. foreach (GameObject g in islandCabs) if (g != null && !(g.name.Length >= 4 && g.name[..4] == "dish") && !string.IsNullOrEmpty(DrawerModelForWidth(AiGet.ObjectWide(g)))) return g; return null; } //----------------------------DISHWASHER---------------------------------------------------------------- public void AddDishwasher() { StartCoroutine(CoroutineAddDishwasher()); } IEnumerator CoroutineAddDishwasher() { // Ordre de préférence : la RANGEE AVANT de la péninsule, puis l'îlot, puis les caissons muraux. // La rangee avant passe devant parce que c'est là que le lave-vaisselle a sa place dans une // péninsule — et ça l'empêche de venir se coller à l'évier sur la rangée murale. // Dans chaque cas on retient le caisson le plus proche de l'évier. Vector3 sink = GetSinkPosition(); GameObject Cabinet = FindClosestPeninsulaReturnCab(sink); if (Cabinet == null) Cabinet = FindClosestBase24(sink, false); // îlot ("floor") if (Cabinet == null) Cabinet = FindClosestBase24(sink, true); // sinon les murs print("[AI] Lave-vaisselle : caisson remplace = " + (Cabinet == null ? "AUCUN" : Cabinet.name)); if (Cabinet != null) { string wallNum = GetWallNumberbyObjectName(Cabinet.name); // "w.." si mural, "floor" si îlot Vector3 PositionToInstall = Cabinet.transform.position; PositionToInstall += Cabinet.transform.forward * 0.3125f; float CabinetWide = AiGet.ObjectWide(Cabinet); float CabinetDepth = AiGet.ObjectDepth(Cabinet); float CabinetHeight = AiGet.ObjectHeight(Cabinet) + _G.caCH; float Angle = AiGet.ObjectAngle(Cabinet); Vector3 Size = new(CabinetWide, CabinetHeight, CabinetDepth); string C42 = GetObjectADN(Cabinet, 42); string C45 = GetObjectADN(Cabinet, 45); string C46 = GetObjectADN(Cabinet, 46); string C47 = GetObjectADN(Cabinet, 47); // Nom du caisson remplace, retenu AVANT sa destruction : les listes de la // peninsule le designent par ce nom. string replacedName = Cabinet.name; int oldIdx = Get.GetObjectIndex(Cabinet.name); DestroyImmediate(Cabinet); if (oldIdx >= 0 && _G.OBJs[oldIdx] != null) _G.OBJs[oldIdx][0] = "null"; // libère l'entrée du caisson remplacé Set_TemporySC_Dishwasher(_AI.DISHWASHER, wallNum, Size, PositionToInstall, Angle); _G.SC[42] = C42; // comptoir + projection _G.SC[45] = C45; // cascade gauche _G.SC[46] = C46; // cascade arrière _G.SC[47] = C47; // cascade droite CreateDISHWASHER.AddDishwasher(); // Le caisson remplace disparait AVEC SON NOM : meme suivi que pour ChangeCabinet. // Le meuble, lui, est toujours la — meme largeur, meme comptoir, meme debord — donc il // reste dans la rangee d'assise. PeninsulaTrackRename(replacedName); DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; ButtonDoneToggle.NotifyDone(); } yield return null; } // Position (XZ) de l'évier : caisson évier (_AI.BASESINK) ou objet "sink". static Vector3 GetSinkPosition() { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[4] == "sink" || o[1] == _AI.BASESINK) return new Vector3(DOIT.ConvertStringToNumber(o[15]), 0f, DOIT.ConvertStringToNumber(o[17])); } return Vector3.zero; } // Caisson de la RANGEE AVANT de la péninsule le plus proche de l'évier, s'il en reste un. Null sinon. // La liste est remplie à la pose ; une entrée dont l'objet a disparu est simplement ignorée. static GameObject FindClosestPeninsulaReturnCab(Vector3 sink) { GameObject best = null; float bestDist = float.MaxValue; foreach (string name in _peninsulaRowCabs) { int idx = Get.GetObjectIndex(name); if (idx < 0 || !DOIT.exist(idx)) continue; GameObject go = Get.o2("SCENE", name); if (go == null) continue; float dx = go.transform.position.x - sink.x; float dz = go.transform.position.z - sink.z; float d = dx * dx + dz * dz; if (d < bestDist) { bestDist = d; best = go; } } return best; } // Caisson 24" (_AI.Base24) le plus proche de 'sink' ; onWall = mur (true) ou îlot (false). Null si aucun. static GameObject FindClosestBase24(Vector3 sink, bool onWall) { GameObject best = null; float bestDist = float.MaxValue; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[2] != "Cabinet" || o[1] != _AI.Base24) continue; bool isWall = o[22] != "floor"; if (isWall != onWall) continue; // Pas de lave-vaisselle trop près du coin de début si le mur précédent y a des caissons // (la porte ne pourrait pas s'ouvrir) : dégagement minimum 60". if (onWall && DishwasherBlockedAtCorner(o)) continue; float x = DOIT.ConvertStringToNumber(o[15]); float z = DOIT.ConvertStringToNumber(o[17]); float dist = (x - sink.x) * (x - sink.x) + (z - sink.z) * (z - sink.z); if (dist < bestDist) { bestDist = dist; best = Get.o2("SCENE", o[0]); } } return best; } // Vrai si ce caisson est à moins de 36" du coin de DÉBUT de son mur ET que le mur précédent a un // caisson/objet à <= 60" de ce coin -> le lave-vaisselle n'aurait pas le dégagement pour ouvrir sa porte. static bool DishwasherBlockedAtCorner(string[] o) { string wall = o[22]; // "w3" Vector3 cornerV = AiGet.WallStartPoint(wall); // début du mur = coin avec le mur précédent Vector2 corner = new(cornerV.x, cornerV.z); Vector2 cab = new(DOIT.ConvertStringToNumber(o[15]), DOIT.ConvertStringToNumber(o[17])); if (Vector2.Distance(cab, corner) >= 36f) return false; // >= 36" du coin -> OK string prevWall = AiGet.PreviousWallName(wall); // "w2" for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] p = _G.OBJs[i]; if (p == null || p.Length < 23) continue; if (p[22] != prevWall) continue; // "caisson ou objet" : tout objet non-ouverture sur le mur précédent. if (p[2] == "Window" || p[2] == "Door" || p[2] == "Opening" || p[2] == "Patio") continue; Vector2 pp = new(DOIT.ConvertStringToNumber(p[15]), DOIT.ConvertStringToNumber(p[17])); if (Vector2.Distance(pp, corner) <= 60f) return true; // objet du mur précédent près du coin } return false; } //----------------------------MICROWAVE--------------------------------------------------------------- public void AddMicrowave() { StartCoroutine(CoroutineAddMicrowave()); } IEnumerator CoroutineAddMicrowave() { // 1) Placer le caisson micro-ondes en remplaçant un caisson existant. // Selon la catégorie de _AI.MICROWAVE : WALL -> caisson mural, BASE -> caisson de base (même largeur). string cat = UIT.Value(Library.Cabinet, _AI.MICROWAVE, Header.Category); bool isWall = !string.IsNullOrEmpty(cat) && cat.ToUpper().Contains("WALL"); float mwW = UIT.ValueFloat(Library.Cabinet, _AI.MICROWAVE, Header.A); // largeur du micro-ondes GameObject toReplace = FindCabinetForMicrowave(isWall, mwW); if (toReplace != null) { ChangeCabinet(toReplace, _AI.MICROWAVE); yield return null; } // 2) Ajouter le visuel micro-ondes sur le(s) caisson(s) micro-ondes. for (int i = 0; i < _G.OBJnum; i++) { if (DOIT.exist(i)) { string[] ADN = _G.OBJs[i]; if (ADN[1] == _AI.MICROWAVE) { GameObject Cabinet = Get.o2("SCENE", ADN[0]); Vector3 PositionToInstall = Cabinet.transform.position; PositionToInstall += 6*Cabinet.transform.up-3*Cabinet.transform.forward; Vector3 Size = new(20, 13, 16); float Angle = AiGet.ObjectAngle(Cabinet); _G.ff = ADN[21]; // from-floor du caisson micro-ondes (utilisé par CreateMICROWAVE pour py) Set_TemporySC_Deco(GetApliancesModel("MICR101"), ADN[22], Size, PositionToInstall, Angle); CreateMICROWAVE.AddMicrowave(); } } } DOIT.AllSelOff(); ButtonDoneToggle.NotifyDone(); yield return null; } // Position (XZ) de la cuisinière (objet "stov"). static Vector3 GetStovePosition() { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (o[4] == "stov") return new Vector3(DOIT.ConvertStringToNumber(o[15]), 0f, DOIT.ConvertStringToNumber(o[17])); } return Vector3.zero; } // Caisson à remplacer par le micro-ondes : mural (isWall) ou base, de MÊME largeur, le plus proche de la cuisinière. static GameObject FindCabinetForMicrowave(bool isWall, float width) { Vector3 stove = GetStovePosition(); GameObject best = null; float bestDist = float.MaxValue; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null ) continue; if (o[2] != "Cabinet") continue; // Type de caisson : "W1" (mural) si isWall, "B1" (base) sinon. string wanted = isWall ? "W1" : "B1"; if (o[4] != wanted) continue; float w = DOIT.ConvertStringToNumber(o[6]); if (Mathf.Abs(w - width) > 0.5f) continue; // même largeur float x = DOIT.ConvertStringToNumber(o[15]); float z = DOIT.ConvertStringToNumber(o[17]); float dist = (x - stove.x) * (x - stove.x) + (z - stove.z) * (z - stove.z); if (dist < bestDist) { bestDist = dist; best = Get.o2("SCENE", o[0]); } } return best; } //----------------------------AJUSTEMENT DES SECTIONS (avant fillers / recover)------------------------------------- // PlaceBaseRun / PlaceWallRun posent les caissons À PARTIR DU DÉBUT de la section : le reste // (< 24") tombe donc TOUJOURS au bout, même quand ce bout est un panneau de frigo ou un caisson // de coin, qui doivent être JOINTS. Exemple : 2 x 24" dans 53 5/8" entre une fenêtre et un panneau // de frigo -> caissons collés à la fenêtre et 5 5/8" de vide contre le panneau. // Cette passe recolle chaque groupe de caissons contigus contre l'obstacle le plus PRIORITAIRE qui // le borde, pour que le jeu résiduel se retrouve du côté neutre (fenêtre, porte, bout de mur) où // ni filler ni panneau de recouvrement n'est requis. À lancer AVANT CoroutineAddFillers. // Bornes verticales des deux rangées (hauteur au-dessus du sol). const float BaseBandTop = 40f; // un objet qui descend plus bas borne la rangée BASSE const float WallBandBase = 50f; // un objet qui monte plus haut borne la rangée HAUTE // Le recentrage du lave-vaisselle raisonne lui sur la rangée PHYSIQUE sous comptoir. Une fenêtre // dont l'appui est au-dessus (le cas normal au-dessus d'un évier) passe par-dessus les caissons : // ce n'est pas un voisin du lave-vaisselle, et l'inclure la faisait passer pour tel dans l'ordre 1D. // La justification, elle, garde la borne large : une section peut être coupée à une fenêtre par // WallSections, et le run doit continuer à s'y arrêter. const float CounterTop = 34f; public void AdjustSections() => StartCoroutine(CoroutineAdjustSections()); IEnumerator CoroutineAdjustSections() { foreach (string wall in WallSelected) { JustifyRunsOnWall(wall, false); // bande BASSE (caissons bas, lave-vaisselle, four...) yield return null; CenterDishwashersOnWall(wall); // rattrape le jeu laissé autour d'un lave-vaisselle yield return null; JustifyRunsOnWall(wall, true); // bande HAUTE (caissons muraux, hotte...) yield return null; } yield return null; } // Un lave-vaisselle n'est pas un caisson : la justification ne le déplace pas, il peut donc se // retrouver avec tout le jeu de la section d'un seul côté quand le groupe voisin a glissé vers un // panneau. Exemple : [évier][lave-vaisselle][caisson 24] ... panneau frigo -> le caisson 24 rejoint // le panneau et laisse un trou contre le lave-vaisselle. // On RECENTRE alors le lave-vaisselle entre ses deux voisins et on ferme les deux jeux résiduels // par un panneau de recouvrement de la largeur du jeu, avec le comptoir prolongé par-dessus. // Les côtés exposés en BOUT de section restent traités par CoroutineAddFillers (panneau de 3/4"). static void CenterDishwashersOnWall(string wall) { const float MinTotal = 1.5f; // en deçà, un simple filler sur le caisson voisin suffit const float MaxTotal = 12f; // au-delà, ce n'est plus un jeu de section : on ne touche à rien string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; // Bande BASSE uniquement : le lave-vaisselle et ses voisins de rangée. var items = new List<(int idx, float x0, float x1)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; if (o[2] == "Splash") continue; if (!ObjectExtentOnWall(i, start, dir, out float x0, out float x1, out float bottom, out _)) continue; if (bottom >= _AI.FloorLine + CounterTop) continue; items.Add((i, x0, x1)); } items.Sort((p, q) => p.x0.CompareTo(q.x0)); // Trace de la rangée telle que la passe la voit : si un lave-vaisselle n'est pas recentré, // c'est ici qu'on voit s'il manque de la liste ou quels sont ses vrais voisins. string row = ""; foreach (var it in items) row += _G.OBJs[it.idx][0] + "[" + it.x0.ToString("F2") + ".." + it.x1.ToString("F2") + "] "; print("[AI] Rangee basse " + wnum + " : " + row); for (int k = 0; k < items.Count; k++) { string[] o = _G.OBJs[items[k].idx]; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (o[5] != "dish" && n4 != "dish") continue; // Voisins RÉELS : l'objet le plus proche entièrement à gauche, et celui entièrement à // droite. On ne peut PAS prendre items[k-1]/items[k+1] : la liste est triée sur x0, et un // objet qui CHEVAUCHE le lave-vaisselle en plan (fenêtre basse, comptoir voisin) s'y // intercale et devient un faux voisin — c'est ce qui faisait sauter le recentrage. int kl = -1, kr = -1; for (int j = 0; j < items.Count; j++) { if (j == k) continue; if (items[j].x1 <= items[k].x0 + 0.5f) { if (kl < 0 || items[j].x1 > items[kl].x1) kl = j; } else if (items[j].x0 >= items[k].x1 - 0.5f) { if (kr < 0 || items[j].x0 < items[kr].x0) kr = j; } } if (kl < 0 || kr < 0) { print("[AI] Lave-vaisselle " + o[0] + " : bout de section (voisin manquant) -> laisse a CoroutineAddFillers"); continue; } string leftName = _G.OBJs[items[kl].idx][0], rightName = _G.OBJs[items[kr].idx][0]; if (IsOpeningObject(leftName) || IsOpeningObject(rightName)) { print("[AI] Lave-vaisselle " + o[0] + " : voisin ouverture (" + leftName + " / " + rightName + ") -> pas de recentrage"); continue; } // Clamp : un voisin dont l'emprise nominale mord légèrement sur le lave-vaisselle ne doit // pas produire un jeu négatif et pousser le recentrage du mauvais côté. float gapL = Mathf.Max(0f, items[k].x0 - items[kl].x1); float gapR = Mathf.Max(0f, items[kr].x0 - items[k].x1); float total = gapL + gapR; if (total <= MinTotal || total > MaxTotal) { print("[AI] Lave-vaisselle " + o[0] + " : jeu total " + total + " hors bornes [" + MinTotal + ", " + MaxTotal + "] (" + leftName + " gapG=" + gapL + " / " + rightName + " gapD=" + gapR + ")"); continue; } GameObject dish = Get.o2("SCENE", o[0]); if (dish == null) continue; float half = total * 0.5f; float shift = (gapR - gapL) * 0.5f; // recentrage : même jeu de chaque côté print("[AI] Lave-vaisselle " + o[0] + " recentre de " + shift + " ; panneau de " + half + " de chaque cote (gapG=" + gapL + " gapD=" + gapR + ")"); if (Mathf.Abs(shift) > 0.01f) { dish.transform.position += dir * shift; DOIT.SaveObjectPositionInData(dish.transform); } // Comptoir prolongé par-dessus les deux panneaux. ADN[42] = "H_gauche_arrière_droite_devant_..." if (!string.IsNullOrEmpty(o[42]) && o[42].Split('_').Length >= 4) { o[42] = DOIT.ChangeCodeOnPos(o[42], 1, half.ToString(), "_"); o[42] = DOIT.ChangeCodeOnPos(o[42], 3, half.ToString(), "_"); } DOIT.REDOCAB(items[k].idx); dish = Get.o2("SCENE", o[0]); // REDOCAB a reconstruit l'objet if (dish == null) continue; AddDishwasherEndPanel(dish, false, half); AddDishwasherEndPanel(dish, true, half); } } // Priorité de l'obstacle qui borde une section : plus la valeur est haute, plus les caissons // doivent être collés contre lui. 0 = neutre (fenêtre, porte, four, frigo, bout de mur) : on ne // déplace JAMAIS vers lui. Une ligne de plus ici suffit à prioriser un autre type d'objet. static int NeighbourPriority(string[] o) { if (o == null || o.Length < 23) return 0; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (SameModel(o[1], _AI.TALLSMALL)) return 6; // petite colonne if (SameModel(o[1], _AI.BASESINK) || IsOverSinkModel(o[1])) return 5; // caisson évier / au-dessus if (o[5] == "PNL" || n4 == "pane") return 4; // panneau (frigo, îlot) if (o[5] == "CB" || o[5] == "CW") return 2; // caisson de coin if (o[5] == "dish" || n4 == "dish") return 1; // lave-vaisselle return 0; } // Caisson que la passe a le droit de déplacer : caisson bas (BA), haut (WA) ou COLONNE (TA). // Une colonne n'est déplaçable que depuis la rangée BASSE : elle traverse les deux bandes, la // rendre déplaçable aussi dans la passe HAUTE la ferait bouger une deuxième fois avec d'autres // voisins. Tout le reste ancre la section : coin (CB/CW), panneau, appareil, ouverture, plus les // caissons dont la position est imposée par autre chose (évier, caisson sur évier, frigo, micro). static bool IsMovableCab(string[] o, bool highBand) { if (o == null || o.Length < 23 || o[2] != "Cabinet") return false; if (o[5] == "TA") return !highBand; if (o[5] != "BA" && o[5] != "WA") return false; if (SameModel(o[1], _AI.BASESINK) || IsOverSinkModel(o[1])) return false; if (SameModel(o[1], _AI.FRIDGE) || SameModel(o[1], _AI.MICROWAVE)) return false; return true; } // Comparaison de modèle sûre : un modèle de librairie non défini ("") ne doit matcher personne. static bool SameModel(string a, string b) => !string.IsNullOrEmpty(a) && a == b; // Emprise d'un objet le long du mur (x0,x1, distances depuis le début du mur) et à la verticale // (bottom,top). Calculée sur les dimensions ADN + les axes du transform, PAS sur les points // PBLB/PBRB : ceux d'un PANNEAU sont posés sur l'axe PROFONDEUR et son PBLU est à mi-hauteur, // ce qui rendait le panneau de frigo invisible pour cette passe. Idem pour une fenêtre, dont les // PBLB sont rentrés de 4" par la moulure alors que ADN[6] donne le cadre extérieur. static bool ObjectExtentOnWall(int idx, Vector3 start, Vector3 dir, out float x0, out float x1, out float bottom, out float top) { x0 = x1 = bottom = top = 0f; string[] o = _G.OBJs[idx]; GameObject g = Get.o2("SCENE", o[0]); if (g == null) return false; float a = DOIT.ConvertStringToNumber(DOIT.CSs(o[6])); // A : largeur (caisson) / épaisseur (panneau) float h = DOIT.ConvertStringToNumber(DOIT.CSs(o[7])); // hauteur float c = DOIT.ConvertStringToNumber(DOIT.CSs(o[8])); // C : profondeur (caisson) / largeur (panneau) if (h <= 0f) return false; // ConstructPanel construit son cube en (C[8], C[7], C[6]) : X local = profondeur, Z local = A. // Tous les autres constructeurs (caisson, four, lave-vaisselle, hotte, fenêtre) sont en (C[6], C[7], C[8]). bool isPanel = o[5] == "PNL"; float alongLocalX = isPanel ? c : a; float alongLocalZ = isPanel ? a : c; float half = 0.5f * (Mathf.Abs(Vector3.Dot(g.transform.right, dir)) * alongLocalX + Mathf.Abs(Vector3.Dot(g.transform.forward, dir)) * alongLocalZ); float cx = Vector3.Dot(g.transform.position - start, dir); x0 = cx - half; x1 = cx + half; bottom = g.transform.position.y - h * 0.5f; top = g.transform.position.y + h * 0.5f; return true; } // Déplace, sur UNE bande d'un mur, chaque groupe de caissons contigus vers l'obstacle le plus // prioritaire qui le borde. highBand = bande des caissons muraux. static void JustifyRunsOnWall(string wall, bool highBand) { const float Touch = 1.0f; // au-delà, deux caissons ne sont plus considérés collés const float MinShift = 0.25f; // en deçà, le décalage ne vaut pas la peine string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; float wallLen = Vector3.Distance(start, AiGet.WallEndPoint(wnum)); // Tout ce qui occupe la bande, projeté sur l'axe du mur. var items = new List<(int idx, float x0, float x1, bool movable)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; if (o[2] == "Splash") continue; // dosseret : ne borde rien if (!ObjectExtentOnWall(i, start, dir, out float x0, out float x1, out float bottom, out float top)) continue; // Bande verticale : un objet qui traverse les deux (colonne, panneau, porte) borde les deux. bool inBand = highBand ? top > _AI.FloorLine + WallBandBase : bottom < _AI.FloorLine + BaseBandTop; if (!inBand) continue; items.Add((i, x0, x1, IsMovableCab(o, highBand))); } if (items.Count == 0) return; items.Sort((p, q) => p.x0.CompareTo(q.x0)); int i0 = 0; while (i0 < items.Count) { if (!items[i0].movable) { i0++; continue; } // Groupe = caissons déplaçables qui se touchent. On suit l'emprise MIN/MAX du groupe et pas // seulement ses extrémités d'index : une colonne suspendue et son caisson de dessous // (_AI.TallUnder) occupent la MÊME travée, l'un est donc entièrement inclus dans l'autre. int i1 = i0; float groupX0 = items[i0].x0, groupX1 = items[i0].x1; while (i1 + 1 < items.Count && items[i1 + 1].movable && items[i1 + 1].x0 - groupX1 <= Touch) { i1++; groupX0 = Mathf.Min(groupX0, items[i1].x0); groupX1 = Mathf.Max(groupX1, items[i1].x1); } // Voisin de GAUCHE : celui dont le bord droit est le plus avancé avant le groupe. Là aussi // l'index précédent ne suffit pas quand des emprises se chevauchent. int kl = -1; for (int j = 0; j < i0; j++) if (kl < 0 || items[j].x1 > items[kl].x1) kl = j; int kr = i1 + 1 < items.Count ? i1 + 1 : -1; float leftEdge = kl >= 0 ? items[kl].x1 : 0f; float rightEdge = kr >= 0 ? items[kr].x0 : wallLen; // GAUCHE : la boucle avance de gauche à droite, donc ce voisin est DÉJÀ traité — sa position // est définitive et sa priorité compte, même s'il était lui-même déplaçable. Sans ça, une // petite colonne ou un caisson évier, déplaçables dans la rangée basse, n'attiraient jamais // rien. Les caissons ordinaires restant à 0, ça n'introduit aucun biais vers la gauche. // DROITE : ce voisin n'est pas encore traité ; s'il est déplaçable il bougera peut-être // lui-même, on ne se colle donc pas dessus. int prioLeft = kl >= 0 ? NeighbourPriority(_G.OBJs[items[kl].idx]) : 0; int prioRight = kr >= 0 && !items[kr].movable ? NeighbourPriority(_G.OBJs[items[kr].idx]) : 0; float slackLeft = groupX0 - leftEdge; float slackRight = rightEdge - groupX1; // Groupe déjà CENTRÉ dans sa section (caisson posé sous une fenêtre par PlaceSingleBase) : // le centrage est voulu, on n'y touche pas. bool centered = slackLeft > MinShift && slackRight > MinShift && Mathf.Abs(slackLeft - slackRight) < 1f; float shift = 0f; if (!centered) { if (prioRight > prioLeft && slackRight > MinShift) shift = slackRight; else if (prioLeft > prioRight && slackLeft > MinShift) shift = -slackLeft; } if (shift != 0f) { print("[AI] Justify " + wnum + (highBand ? " haut" : " bas") + " : " + _G.OBJs[items[i0].idx][0] + ".." + _G.OBJs[items[i1].idx][0] + " decale de " + shift + " (prioG=" + prioLeft + " prioD=" + prioRight + ")"); for (int k = i0; k <= i1; k++) { MoveCabAlongWall(items[k].idx, dir * shift); items[k] = (items[k].idx, items[k].x0 + shift, items[k].x1 + shift, true); } } i0 = i1 + 1; } } // Déplace un objet le long du mur et met à jour son ADN (position absolue + offset mur-relatif). // Comptoir, portes, dosseret et points PBLB/PBRB sont enfants de la racine du caisson : aucune // reconstruction n'est nécessaire, contrairement à IncreaseBaseCab qui change de modèle. static void MoveCabAlongWall(int idx, Vector3 offset) { GameObject g = Get.o2("SCENE", _G.OBJs[idx][0]); if (g == null) return; g.transform.position += offset; DOIT.SaveObjectPositionInData(g.transform); } //----------------------------SENS D'OUVERTURE DES PORTES------------------------------------------- // Objet de référence dont un caisson voisin tire son sens d'ouverture. // Rangée BASSE (caissons bas et COLONNES) : four, frigo, caisson évier. // Rangée HAUTE (caissons muraux) : fenêtre d'évier et hotte. static bool IsHandleReference(string[] o, bool highBand, int sinkWindowIdx) { if (o == null || o.Length < 23) return false; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (highBand) { if (n4 == "hood") return true; return sinkWindowIdx != 1000 && _G.OBJs[sinkWindowIdx] != null && o[0] == _G.OBJs[sinkWindowIdx][0]; } if (n4 == "frid" || n4 == "stov") return true; return o[4] == "sink" || SameModel(o[1], _AI.BASESINK); } // Sens d'ouverture d'un caisson voisin d'une référence : "R" s'il est à GAUCHE de celle-ci, // "L" s'il est à DROITE. ADN[23] == "F" (deux portes) n'est jamais touché — et surtout jamais // passé à FlipCabinet, qui basculerait un "F" en "R". // À lancer AVANT CoroutineAddFillers : fillers et panneaux de recouvrement lisent ADN[23]. public void FlipHandles() => StartCoroutine(CoroutineFlipHandles()); IEnumerator CoroutineFlipHandles() { int sinkWindowIdx = GetWindowSinkIndex(); // référence de la rangée HAUTE, avec la hotte foreach (string wall in WallSelected) { FlipHandlesOnWall(wall, false, sinkWindowIdx); // bas + colonnes : four, frigo, évier yield return null; FlipHandlesOnWall(wall, true, sinkWindowIdx); // muraux : fenêtre d'évier, hotte yield return null; } yield return null; } static void FlipHandlesOnWall(string wall, bool highBand, int sinkWindowIdx) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; var items = new List<(int idx, float x0, float x1)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; if (o[2] == "Splash") continue; if (!ObjectExtentOnWall(i, start, dir, out float x0, out float x1, out float bottom, out float top)) continue; bool inBand = highBand ? top > _AI.FloorLine + WallBandBase : bottom < _AI.FloorLine + BaseBandTop; if (!inBand) continue; items.Add((i, x0, x1)); } items.Sort((p, q) => p.x0.CompareTo(q.x0)); // Références de cette bande sur ce mur. var refs = new List(); for (int k = 0; k < items.Count; k++) if (IsHandleReference(_G.OBJs[items[k].idx], highBand, sinkWindowIdx)) refs.Add(k); if (refs.Count == 0) return; // TOUS les caissons du mur sont orientés, pas seulement ceux collés à une référence. Chacun // suit la référence la PLUS PROCHE : sur un mur à plusieurs références (four et évier, par ex.) // c'est ce qui départage sans se contredire. for (int k = 0; k < items.Count; k++) { if (refs.Contains(k)) continue; // Seuls les CAISSONS ont une poignée : sans ce filtre la boucle calculait un côté pour les // ouvertures, fenêtres, panneaux, lave-vaisselle et appareils, que SetHandleSide écartait // ensuite en silence — la trace en devenait illisible. if (_G.OBJs[items[k].idx][2] != "Cabinet") continue; int bestRef = -1; float bestDist = float.MaxValue; bool towardWallStart = false; // le caisson est-il du côté du DÉBUT du mur ? foreach (int r in refs) { float d; bool atStartSide; if (items[k].x1 <= items[r].x0 + 0.5f) { d = items[r].x0 - items[k].x1; atStartSide = true; } else if (items[k].x0 >= items[r].x1 - 0.5f) { d = items[k].x0 - items[r].x1; atStartSide = false; } else continue; // chevauche la référence en plan : pas concerné if (d < bestDist) { bestDist = d; bestRef = r; towardWallStart = atStartSide; } } if (bestRef < 0) continue; // RÈGLE : la poignée est du côté de la RÉFÉRENCE. Un caisson placé du côté du DÉBUT du mur // a donc sa référence côté FIN, et sa poignée doit pointer par là. // HandleTowardWallEnd nomme le code qui met la poignée vers la FIN du mur ; l'axe du mur // étant opposé au sens vu depuis la pièce, c'est "L". Un seul endroit à inverser si le // rendu contredit ça. const string HandleTowardWallEnd = "L"; const string HandleTowardWallStart = "R"; string wanted = towardWallStart ? HandleTowardWallEnd : HandleTowardWallStart; print("[AI] Poignee " + _G.OBJs[items[k].idx][0] + " : " + (towardWallStart ? "cote DEBUT" : "cote FIN") + " de " + _G.OBJs[items[bestRef].idx][0] + " (a " + bestDist + ") -> " + wanted); SetHandleSide(items[k].idx, wanted, _G.OBJs[items[bestRef].idx][0], highBand); } } // Met ADN[23] du caisson à 'side' en passant par CONTROL.FlipCabinet, qui bascule aussi les recettes // de portes (ADN[25]/[26]) et les modèles de coin. Ne touche qu'un caisson déjà en "L" ou "R". static void SetHandleSide(int idx, string side, string refName, bool highBand) { string[] o = _G.OBJs[idx]; if (o == null || o.Length < 24 || o[2] != "Cabinet") return; // Rangée BASSE : caissons bas et colonnes. Rangée HAUTE : caissons muraux SEULEMENT — une // colonne traverse les deux bandes et ne doit être orientée que par la règle du bas, sinon la // hotte viendrait contredire le four. if (highBand ? o[5] != "WA" : (o[5] != "BA" && o[5] != "TA")) return; // Modèles à FAÇADE IMPOSÉE : caisson micro-ondes, caisson au-dessus du frigo, caisson évier et // caisson au-dessus de l'évier. Leur devant n'est pas une porte à main gauche ou droite, il n'y // a rien à retourner — mêmes exclusions que IsMovableCab. if (SameModel(o[1], _AI.MICROWAVE) || SameModel(o[1], _AI.FRIDGE) || SameModel(o[1], _AI.BASESINK) || IsOverSinkModel(o[1])) { print("[AI] Poignee " + o[0] + " ignoree : modele a facade imposee (" + o[1] + ")"); return; } if (o[23] != "L" && o[23] != "R") { // DEUX portes (ADN[26] renseigné, même test que CONTROL.FLIP) : aucun côté à choisir. if (!string.IsNullOrEmpty(o[26])) { print("[AI] Poignee " + o[0] + " ignoree : deux portes (ADN[23]=" + o[23] + ")"); return; } // UNE seule porte mais aucun côté déclaré dans la librairie — c'est le cas des caissons de // finition depuis qu'ils ne forcent plus "R". Rester sur "F" les excluait de l'orientation // et laissait les bouts de section dans le mauvais sens. On amorce donc au côté OPPOSÉ pour // que FlipCabinet fasse ensuite le travail complet : ADN[23], recette de porte, rebuild. o[23] = side == "L" ? "R" : "L"; print("[AI] Poignee " + o[0] + " : cote non declare, amorce a " + o[23]); } if (o[23] == side) return; // déjà du bon côté print("[AI] Poignee " + o[0] + " : " + o[23] + " -> " + side + " (voisin de " + refName + ")"); // Pas de REDOCAB supplémentaire ici : FlipCabinet en fait déjà un, et ConstructBASE réinverse // ADN[25] à chaque reconstruction d'un caisson "R" — un second passage annulerait le flip. CONTROL.FlipCabinet(idx); } //----------------------------ADD FILLERS & RECOVER---------------------------------------------------------------- public void AddFillers_Cover() { StartCoroutine(CoroutineAddFillers()); } // Une ouverture ne reçoit jamais de filler : le jeu qui la borde appartient à son encadrement. // Nom vide (le rayon n'a rien touché d'identifiable) -> on ne pose rien non plus. static bool IsOpeningObject(string objName) { if (string.IsNullOrEmpty(objName) || objName.Length < 4) return true; string n = objName[..4]; return n == "wind" || n == "door" || n == "open" || n == "pati"; } // Le voisin porte-t-il déjà un filler sur la face qui nous regarde ? fillerIndex = 50 (gauche) // ou 51 (droite) DU VOISIN. Couvre les fillers posés par une passe antérieure. static bool NeighbourHasFiller(string objName, int fillerIndex) { int n = Get.GetObjectIndex(objName); if (n < 0 || n >= _G.OBJs.Length) return false; string[] o = _G.OBJs[n]; if (o == null || o.Length <= fillerIndex) return false; string code = o[fillerIndex]; return !string.IsNullOrEmpty(code) && code.Split('?')[0] != "NONE"; } // ADN[50] / ADN[51] = "FLAT?finition?90?largeur" quand un filler est posé, "NONE" (ou vide) sinon. static bool HasFillerCode(string code) => !string.IsNullOrEmpty(code) && code.Split('?')[0] != "NONE"; // ADN[81] = "0_0_0_0_0_0_0" tant qu'aucun panneau de recouvrement n'est posé. static bool HasRecoverCode(string code) => !string.IsNullOrEmpty(code) && code.Replace("_", "").Replace("0", "").Length > 0; // Un jeu n'est comblé qu'UNE fois : la paire {caisson, voisin} sert de clé, quel que soit l'ordre // dans lequel la boucle rencontre les deux objets. Évite le doublon filler droit + filler gauche. static bool ClaimGap(HashSet filled, string self, string neighbour) { if (string.IsNullOrEmpty(neighbour)) return false; string key = string.CompareOrdinal(self, neighbour) <= 0 ? self + "|" + neighbour : neighbour + "|" + self; return filled.Add(key); } IEnumerator CoroutineAddFillers() { _G.CameraCliping = 1; Camera.main.GetComponent().Hide(); HashSet filledGaps = new(); // jeux déjà comblés, clé = paire {caisson, voisin} for (int i = 0; i < _G.OBJnum; i++) { print("i======" + i); if (DOIT.exist(i)) { string[] ADN = _G.OBJs[i]; print("i=====exist=" + i+" ADN[2]==="+ADN[2]+" ADN[22]==="+ADN[22]); if (ADN[2] == "Cabinet" && ADN[22].Contains("w") && ADN[4]!="B2" && ADN[4]!="W2" && ADN[4]!="W4")//B2 is off only for demo { float distanceRight = AiGet.GetSpaceSide(ADN, "Right", out string rightObj); yield return null; float distanceLeft = AiGet.GetSpaceSide(ADN, "Left", out string leftObj); yield return null; string IDText = Match_Finish.Exterior_From_Door(ADN[61]).Split("?")[1]; print("IDText======" + IDText + "Cabinet " + ADN[0] + " distanceRight= " + distanceRight + " (" + rightObj + ") distanceLeft= " + distanceLeft + " (" + leftObj + ")"); // Un jeu se comble des DEUX côtés : le voisin peut être un panneau, un lave-vaisselle // ou un appareil, qui ne posent jamais de filler de leur côté. Conditions communes : // jeu exploitable, voisin identifié qui n'est pas une ouverture, voisin sans filler // sur la face qui nous regarde, et jeu pas déjà comblé par l'autre caisson. bool doRight = distanceRight > 0.25f && distanceRight < 6 && !IsOpeningObject(rightObj) && !NeighbourHasFiller(rightObj, 50) // face gauche du voisin de droite && ClaimGap(filledGaps, ADN[0], rightObj); bool doLeft = distanceLeft > 0.25f && distanceLeft < 6 && !IsOpeningObject(leftObj) && !NeighbourHasFiller(leftObj, 51) // face droite du voisin de gauche && ClaimGap(filledGaps, ADN[0], leftObj); if (doRight) { string wide = distanceRight.ToString(); ADN[51] = "FLAT?" + IDText + "?90?" + wide; ADN[38] = distanceRight.ToString(); ADN[42] = DOIT.ChangeCodeOnPos(ADN[42], 3, wide, "_"); ADN[55] = DOIT.ChangeCodeOnPos(ADN[55], 2, wide, ","); ADN[63] = DOIT.ChangeCodeOnPos(ADN[63], 1, IDText, "?"); } if (doLeft) { string wide = distanceLeft.ToString(); ADN[50] = "FLAT?" + IDText + "?90?" + wide; ADN[37] = distanceLeft.ToString(); ADN[42] = DOIT.ChangeCodeOnPos(ADN[42], 1, wide, "_"); ADN[55] = DOIT.ChangeCodeOnPos(ADN[55], 0, wide, ","); ADN[63] = DOIT.ChangeCodeOnPos(ADN[63], 1, IDText, "?"); } if (doRight || doLeft) { DOIT.REDOCAB(i); // une seule reconstruction, même si les deux côtés sont comblés yield return new WaitForSecondsRealtime(1); } if (distanceRight == 1000 || distanceLeft == 1000) { string Model = "4"; if (_G.PATH == "collection_elite") { Model = "3"; if (ADN[1] == _AI.TALL) { Model = "2"; } } if (_G.PATH == "baindepot") { switch (ADN[4]) { case "B1":Model="3";break; case "W1":Model="9";break; case "T1":Model="1";break; } } string PanelRecipe = Model + "?" + IDText + "?0";//Model+ColorIF+Sens print("Add recover On Right to Cabinet ====" + PanelRecipe+" _G.PATH====="+_G.PATH); if (distanceRight == 1000) { if (new List { "B1", "W1", "T1" }.Contains(ADN[4])) { ADN[81] = "0_0_" + PanelRecipe + "_0_0_0_0"; // Caisson de BAS avec recouvrement : le comptoir déborde de 1" de ce côté // pour couvrir l'épaisseur du panneau. ADN[42] = "H_left_back_right_front_..." // -> position 3 = droite. Set.NewValueCode RETOURNE la recette, il faut l'assigner. if(ADN[4]=="B1"){ ADN[42] = Set.NewValueCode(ADN[42], "1", 3, "_"); } } else { ADN[81] = "0_0_0_" + PanelRecipe + "_0_0_0"; } } if (distanceLeft == 1000) { if (new List { "B1", "W1", "T1" }.Contains(ADN[4])) { ADN[81] = Set.NewValueCode(ADN[81], PanelRecipe, 0, "_"); // Idem côté gauche : position 1 de ADN[42]. if(ADN[4]=="B1"){ ADN[42] = Set.NewValueCode(ADN[42], "1", 1, "_"); } } } DOIT.REDOCAB(i); } } else if (ADN[0] != null && ADN[0].Length >= 4 && ADN[0][..4] == "dish" && ADN[22].Contains("w")) { // Lave-vaisselle en bout de section : panneau de recouvrement sur le côté exposé, comme un caisson. GameObject dish = Get.o2("SCENE", ADN[0]); if (dish != null) { float dRight = AiGet.GetSpaceSide(ADN, "Right"); yield return null; float dLeft = AiGet.GetSpaceSide(ADN, "Left"); yield return null; if (dRight == 1000) { AddDishwasherEndPanel(dish, true); yield return new WaitForSecondsRealtime(0.5f); } if (dLeft == 1000) { AddDishwasherEndPanel(dish, false); yield return new WaitForSecondsRealtime(0.5f); } } } } } yield return null; } // Panneau de recouvrement sur le côté exposé d'un lave-vaisselle, comme un caisson. // panelWide = épaisseur du panneau le long du mur : 3/4" en bout de section (côté simplement // exposé), ou la largeur du jeu à combler quand le lave-vaisselle a été recentré entre deux voisins. static void AddDishwasherEndPanel(GameObject dish, bool rightSide, float panelWide = 0.75f) { Vector3 pos = dish.transform.position; Vector3 right = dish.transform.right.normalized; float wide = AiGet.ObjectWide(dish); float depth = AiGet.ObjectDepth(dish); float height = AiGet.ObjectHeight(dish); float angle = AiGet.ObjectAngle(dish); string wall = GetWallNumberbyObjectName(dish.name); Vector3 dir = rightSide ? right : -right; Vector3 size = new(panelWide, height, depth); Vector3 panelPos = pos + (wide * 0.5f + panelWide * 0.5f) * dir; Set_TemporySC_Panel(_AI.PANEL_FRIDGE, wall, size, panelPos, angle - 90, "F"); _G.SC[60] = "GLOBAL1"; _G.SC[61] = UIT.Global(Header.Door, 1).Split("?")[1] + "?0"; _G.SC[60] = "GLOBAL2"; CreatePnl.addpnl(); } //----------------------------STOOLS--------------------------------------------------------------- public void AddStools() { StartCoroutine(CoroutineAddStools()); } IEnumerator CoroutineAddStools() { // La FONCTION du meuble décide, pas sa seule présence : sans "Stool" dans _G.Island, // aucun tabouret. Avant, tout îlot ou toute péninsule en recevait. if (!WantsStools()) { print("[AI] Island = " + _G.Island + " -> aucun tabouret (fonction Stool absente)."); yield break; } //List Objects List all = GetAllObjectsOnFloor("floor", "All", "Base_Dish", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine, "From Start"); // "StoolR" : l'assise n'est pas le long de l'allee mais AU BOUT du L. La rangee qui // porte les tabourets est alors celle du RETOUR, et c'est elle qui a recu le debord de 12". // La rangee d'assise du COTE BOUT, c'est le retour du L **et le COIN 2** : le coin ferme // le retour cote mur, il porte le meme debord et un tabouret lui fait face. // AVANT, la condition exigeait au moins un caisson de retour. Or le retour se reduit souvent au // seul coin 2 — la recherche accepte n == 0, et en "StoolR" les 12" de debord a degager la // rendent d'autant plus stricte. La rangee d'assise etait alors jugee vide, on retombait // sur la rangee avant, et les tabourets s'alignaient sur toute la largeur du COTE AVANT. bool returnSeating = WantsStoolsAtReturn() && _peninsulaCornerCabs.Count >= 2; List row = null; if (returnSeating) { row = new List(); foreach (string n in _peninsulaReturnCabs) { GameObject o = Get.o2("SCENE", n); if (o != null) row.Add(o); else print("[AI] Tabourets : caisson de retour \"" + n + "\" introuvable dans la " + "scene -> il manque a la rangee d'assise et au compte."); } GameObject c2 = Get.o2("SCENE", _peninsulaCornerCabs[_peninsulaCornerCabs.Count - 1]); if (c2 != null) row.Add(c2); if (row.Count == 0) { print("[AI] Tabourets : \"StoolR\" mais ni retour ni coin 2 retrouves dans la scene " + "-> repli sur l'assise du COTE AVANT."); returnSeating = false; row = null; } else { print("[AI] Tabourets : assise du COTE BOUT (StoolR) sur " + _peninsulaReturnCabs.Count + " caisson(s) de retour + le coin 2."); } } if (row == null) { // Une rangée (les caissons ARRIÈRE si îlot double) -> longueur non doublée + côté banquette. row = GetIslandBackRow(all); } // Pas d'îlot -> pas de tabourets (évite IndexOutOfRange sur row[0]). if (row == null || row.Count == 0) yield break; // Le retour du L n'est pas la rangee arriere d'un ilot double : sans ce garde-fou, le seul // fait qu'il ne couvre pas toute la liste 'all' aurait retourne le cote de l'assise. bool isDouble = !returnSeating && row.Count < all.Count; Vector3 rowCenter = GetIslandCenterPoint(row); float LengthAvailable = GetIslandWide(row); // Côté PROJECTION (banquette) : // - double -> DEVANT les portes de la rangée arrière, // - simple -> DERRIÈRE les caissons (côté opposé). Vector3 doorDir = AiGet.FowardbyObject(row[0]).normalized; Vector3 seatingDir = isDouble ? -doorDir : doorDir; // PENINSULE : l'assise est du COTE AVANT — la face qui longe l'allee et qui porte le // debord de comptoir. La convention d'ILOT appliquee ici ("simple -> derriere les // caissons") envoyait les tabourets du COTE INTERIEUR, contre la rangee murale : un ilot // est entoure de vide et les deux faces se valent, une peninsule non. // Restreint au cas peninsule : un ilot ordinaire garde exactement son comportement. // PENINSULE, assise le long de l'allee : le cote est celui des PORTES, sans detour. // AVANT, on inversait la regle d'ilot — et cette regle passe par isDouble, que // GetIslandBackRow deduit de l'ECART DE PROFONDEUR entre les caissons. Or le retour du L est // perpendiculaire a la rangee avant et creusait cet ecart : tant que le retour portait un caisson, // isDouble valait vrai et l'inversion retombait par chance sur le bon cote ; des que le // retour a disparu, isDouble est passe faux et les tabourets ont saute sur la face opposee. // Le resultat ne doit dependre ni du retour ni d'une heuristique d'ilot double. if (_peninsulaCornerCabs.Count > 0) { seatingDir = doorDir; print("[AI] Tabourets : peninsule -> assise du COTE AVANT (cote des portes), " + "independamment de la detection d'ilot double."); } Vector3 Direction = -GetDirectionbyObject(row[0]); float Angle = AiGet.ObjectAngle(row[0]); Vector3 Size = new(12, 30, 12); // ASSISE SUR LE RETOUR ("StoolR"). Les deux axes ne se deduisent plus des aides d'objet : // le retour est tourne de 90 par rapport a la rangee avant, et FowardbyObject / GetDirectionbyObject // y ressortaient l'un pour l'autre. L'assise partait donc LE LONG de la rangee au lieu de // sa normale — d'ou des tabourets au COTE INTERIEUR, au bout du retour, au lieu du COTE BOUT. // Le simple changement de sens que j'avais tente n'y pouvait rien : l'axe lui-meme etait // le mauvais. // Les deux caissons de coin donnent la geometrie exacte, sans passer par l'orientation // d'un caisson : l'axe COIN 1 -> COIN 2 est celui de la rangee avant, donc la NORMALE du retour. La // rangee des tabourets suit la perpendiculaire. // SENS : vers l'EXTERIEUR du L, au-dela du COTE BOUT — donc en s'ELOIGNANT du coin 1. // C'est la meme regle que du COTE AVANT, ou l'assise est sous le bord exterieur de la // rangee de la rangee avant : on ne s'assoit jamais dans la section disponible, qui est l'allee de // travail. J'avais d'abord fait pointer le sens VERS le coin 1, ce qui mettait les // tabourets dans l'allee. if (returnSeating && _peninsulaCornerCabs.Count >= 2) { GameObject c1 = Get.o2("SCENE", _peninsulaCornerCabs[0]); GameObject c2 = Get.o2("SCENE", _peninsulaCornerCabs[1]); if (c1 != null && c2 != null) { Vector3 v = c2.transform.position - c1.transform.position; v.y = 0f; if (v.sqrMagnitude > 0.01f) { seatingDir = v.normalized; Direction = Vector3.Cross(Vector3.up, seatingDir).normalized; print("[AI] Tabourets : retour du L -> assise au-dela du COTE BOUT (" + seatingDir.x.ToString("0.00") + "," + seatingDir.z.ToString("0.00") + "), rangee sur (" + Direction.x.ToString("0.00") + "," + Direction.z.ToString("0.00") + ") — axes pris sur les deux coins."); } } } // Placement selon la PROJECTION du comptoir (débord banquette), pour les deux types. // rowCenter = centre de la face arrière (PBLB/PBRB). Distance jusqu'à la face banquette : // - simple : 0 (rowCenter est déjà la face banquette), // - double : profondeur complète (rowCenter = face intérieure, banquette côté opposé). float depth = AiGet.ObjectDepth(row[0]); float faceDist = isDouble ? depth : 0f; // PENINSULE : cette distance se MESURE au lieu d'etre deduite. Selon l'orientation des // caissons de la rangee avant ou du retour, PBLB/PBRB tombe tantot sur la face d'assise, tantot sur // l'opposee — et le faceDist d'ilot laissait les tabourets a une profondeur de caisson de // trop, soit une trentaine de pouces du meuble. // On prend la face la plus AVANCEE de la rangee sur l'axe d'assise : centre du caisson // plus sa demi-profondeur. Le resultat ne depend plus de ce que designe PBLB. if (_peninsulaCornerCabs.Count > 0) { float faceOut = float.MinValue; foreach (GameObject o in row) { if (o == null) continue; float half = AiGet.ObjectDepth(o) * 0.5f; faceOut = Mathf.Max(faceOut, Vector3.Dot(o.transform.position - rowCenter, seatingDir) + half); } if (faceOut > float.MinValue) { print("[AI] Tabourets : distance mesuree jusqu'a la face d'assise = " + faceOut + " (au lieu de " + faceDist + " deduit de la regle d'ilot)."); faceDist = faceOut; } } // Débord de comptoir = plus grande projection (avant/arrière) du recette SC[42] "H_left_back_right_front_...". string[] cp = (GetObjectADN(row[0], 42) ?? "").Split('_'); float projection = (cp.Length > 4) ? Mathf.Max(DOIT.ConvertStringToNumber(cp[2]), DOIT.ConvertStringToNumber(cp[4])) : _G.CounterFront; // Les deux caissons de COIN bornent l'assise au meme titre que la rangee avant, mais ils // n'entrent pas dans 'row' : GetAllObjectsOnFloor ne retient que les modeles B1/dish, or un // coin est un B9/B10 — et le coin 1 est porte par le MUR, pas par "floor". La longueur // disponible s'arretait donc au premier et au dernier caisson de la rangee avant, et les tabourets // se tassaient au milieu. // On les ajoute par leur EMPRISE mesuree sur l'axe des tabourets, sans les verser dans // 'row' : GetIslandBackRow y lit la profondeur pour reconnaitre un ilot double, et le coin // 2, decale sur l'autre branche du L, lui aurait fait couper la rangee en deux. if (_peninsulaCornerCabs.Count > 0) { float lo = float.MaxValue, hi = float.MinValue; void Span(GameObject o) { if (o == null) return; // ASSISE DU COTE BOUT : on prend l'EMPRISE COMPLETE du caisson sur l'axe de la // rangee, largeur et profondeur combinees selon son orientation. // PBLB/PBRB sont deux points d'UNE SEULE face. Pour le coin 2, dont cette face est // perpendiculaire a l'axe du retour, leur projection s'effondre a presque zero : ses // 36" ne comptaient pour rien, la longueur tombait sous le seuil de 30" et // Qty = round(longueur / 20) ne donnait qu'un seul tabouret. if (returnSeating) { Vector3 fw = AiGet.FowardbyObject(o).normalized; // axe profondeur Vector3 sd = GetDirectionbyObject(o).normalized; // axe largeur float half = Mathf.Abs(Vector3.Dot(sd, Direction)) * AiGet.ObjectWide(o) * 0.5f + Mathf.Abs(Vector3.Dot(fw, Direction)) * AiGet.ObjectDepth(o) * 0.5f; float c = Vector3.Dot(o.transform.position - rowCenter, Direction); lo = Mathf.Min(lo, c - half); hi = Mathf.Max(hi, c + half); return; } foreach (string tag in new[] { "PBLB", "PBRB" }) { Transform t = o.transform.Find(tag); if (t == null) continue; float v = Vector3.Dot(t.position - rowCenter, Direction); lo = Mathf.Min(lo, v); hi = Mathf.Max(hi, v); } } foreach (GameObject o in row) Span(o); // Assise le long de l'allee : les DEUX coins bornent la rangee. Assise au bout : seul // le coin 2 la borne — le coin 1 est a l'autre extremite du L et n'a rien a y voir. int firstCorner = returnSeating ? _peninsulaCornerCabs.Count - 1 : 0; for (int ci = firstCorner; ci < _peninsulaCornerCabs.Count; ci++) Span(Get.o2("SCENE", _peninsulaCornerCabs[ci])); if (hi > lo) { // Meme convention que GetIslandWide : l'emprise moins les 12" de garde. float widened = (hi - lo) - 12f; // COTE BOUT : la longueur reste celle de GetIslandWide(row), c'est-a-dire la SOMME // DES LARGEURS du coin 2 et des caissons du retour, moins la garde — 36 + 24 - 12 = // 48, donc deux tabourets. Elle ne depend d'aucune convention d'axe. // L'etendue projetee, elle, en depend : selon l'orientation de chaque caisson elle // retombait sous 30" et l'arrondi de Qty = longueur / 20 basculait a UN seul // tabouret. On ne s'en sert donc plus que pour RECENTRER la rangee, ou une erreur // de quelques pouces ne se voit pas. // COTE AVANT : rien ne change, les coins n'y sont pas dans 'row' et c'est // l'etendue qui les fait entrer dans le compte. if (!returnSeating) { print("[AI] Tabourets : longueur " + LengthAvailable + " -> " + widened + " en incluant " + _peninsulaCornerCabs.Count + " caisson(s) de coin."); LengthAvailable = widened; } else { print("[AI] Tabourets : longueur " + LengthAvailable + " (somme des largeurs coin 2 + retour), etendue mesuree " + widened + " gardee pour le recentrage seulement."); } rowCenter += Direction * ((lo + hi) * 0.5f); } } // Tabouret 6" au-delà du bord de la projection sur un ILOT, ou il est degage de toutes // parts. Sur une PENINSULE il se glisse SOUS le comptoir : il se colle au bord du debord // au lieu de s'en ecarter, soit 6" de moins. float stoolGap = _peninsulaCornerCabs.Count > 0 ? 0f : 6f; Vector3 basePos = rowCenter + seatingDir * (faceDist + projection + stoolGap); basePos.y = -_G.HEIGHT * 0.5f + 15; // Quantité = longueur/20 ; marges de 8" (réduites si l'îlot est court) ; répartition uniforme. int Qty = Mathf.Max(1, Mathf.RoundToInt(LengthAvailable / 20f)); float margin = Mathf.Min(8f, LengthAvailable * 0.25f); float span = LengthAvailable - 2f * margin; float step = (Qty > 1) ? span / (Qty - 1) : 0f; Vector3 startPos = basePos - Direction * (LengthAvailable * 0.5f); print("[AI] Tabourets : " + Qty + " pose(s) sur " + (returnSeating ? "le RETOUR du L (cote Bout)" : "la face AVANT de la rangee avant (allee)") + ", rangee de " + row.Count + " caisson(s), longueur " + LengthAvailable + ", debord lu sur ADN[42] = " + projection + ", centre " + rowCenter.x.ToString("0.0") + "," + rowCenter.z.ToString("0.0") + ", assise vers " + seatingDir.x.ToString("0.0") + "," + seatingDir.z.ToString("0.0") + "."); for (int i = 0; i < Qty; i++) { float d = (Qty == 1) ? LengthAvailable * 0.5f : margin + step * i; Vector3 stoolPos = startPos + Direction * d; Set_TemporySC_Deco("STOO101", "floor", Size, stoolPos, Angle); CreateStool.AddStool(); yield return new WaitForSecondsRealtime(0.5f); } yield return null; } //----------------------------LAMPS---------------------------------------------------------------- public void AddLamps() { StartCoroutine(CoroutineAddLamps()); } // Degagement reserve au bout du COTE MUR pour les caissons de HAUT : une suspension placee // sous eux viendrait buter dedans. 24", soit le double de leur profondeur — ce n'est donc pas // la profondeur du caisson mais la place a laisser devant lui. const float LampWallCabinetClearance = 24f; // Caissons du COTE AVANT de la peninsule : la rangee avant ET les deux caissons de coin. // _peninsulaRowCabs porte la rangee avant ET le retour ; on retire le retour, puis on ajoute // les coins, absents des listes "floor" — un coin est un B9/B10 que le filtre de modele ecarte, // et le coin 1 est porte par le MUR, pas par "floor". static List PeninsulaFrontRow() { var list = new List(); if (_peninsulaCornerCabs.Count == 0) return list; foreach (string n in _peninsulaRowCabs) { if (_peninsulaReturnCabs.Contains(n)) continue; GameObject o = Get.o2("SCENE", n); if (o != null) list.Add(o); } foreach (string n in _peninsulaCornerCabs) { GameObject o = Get.o2("SCENE", n); if (o != null) list.Add(o); } return list; } IEnumerator CoroutineAddLamps() { //List Objects List ListObjectsOnIsland = GetAllObjectsOnFloor("floor", "All", "Base_Dish", _AI.FloorLine + _AI.CabinetHeight, _AI.FloorLine, "From Start"); // PENINSULE : les suspensions se repartissent sur le COTE AVANT — la rangee avant et les // deux coins. Sans ca on retombe sur la mecanique d'ilot, qui ecarte les coins et prend le // RETOUR, perpendiculaire, pour la seconde rangee d'un ilot double. List frontRow = PeninsulaFrontRow(); if (frontRow.Count > 0) { ListObjectsOnIsland = frontRow; print("[AI] Lampes : peninsule -> repartition sur le COTE AVANT, " + frontRow.Count + " caisson(s) coins compris."); } else { //Îlot double : ne garder qu'une rangée (caissons arrière) pour ne pas doubler la longueur. ListObjectsOnIsland = GetIslandBackRow(ListObjectsOnIsland); } Vector3 Size = new(6, 36, 6); float Angle = 0f; // Lampes AU-DESSUS DE L'ÎLOT : seulement s'il y a un îlot (évite IndexOutOfRange sur [0]). if (ListObjectsOnIsland != null && ListObjectsOnIsland.Count > 0) { //Find center point of Island Vector3 PositionToInstall = GetIslandCenterPoint(ListObjectsOnIsland); //Find Lenght of Island float LengthAvailable = GetIslandWide(ListObjectsOnIsland); //Get Directions Vector3 Forward = -AiGet.FowardbyObject(ListObjectsOnIsland[0]); Vector3 Direction = -GetDirectionbyObject(ListObjectsOnIsland[0]); Angle = AiGet.ObjectAngle(ListObjectsOnIsland[0]); // COTE AVANT mesure sur les EMPRISES REELLES — largeur et profondeur combinees selon // l'orientation — puis moins la profondeur des caissons MURAUX au bout du Cote Mur. // GetIslandWide somme les seules largeurs : pour un coin tourne, ou dont la face est // perpendiculaire a l'axe, la valeur ne veut rien dire. if (frontRow.Count > 0) { float lo = float.MaxValue, hi = float.MinValue; foreach (GameObject o in frontRow) { if (o == null) continue; Vector3 fw = AiGet.FowardbyObject(o).normalized; // axe profondeur Vector3 sd = GetDirectionbyObject(o).normalized; // axe largeur float half = Mathf.Abs(Vector3.Dot(sd, Direction)) * AiGet.ObjectWide(o) * 0.5f + Mathf.Abs(Vector3.Dot(fw, Direction)) * AiGet.ObjectDepth(o) * 0.5f; float c = Vector3.Dot(o.transform.position - PositionToInstall, Direction); lo = Mathf.Min(lo, c - half); hi = Mathf.Max(hi, c + half); } if (hi > lo) { float avant = hi - lo; // Le degagement se prend AUX DEUX BOUTS, pas une fois pour toutes : au COTE // MUR pour les caissons de haut, et a l'autre extremite de la meme facon. // En ne retranchant qu'une fois, la reduction se repartissait de part et // d'autre du centre et ne laissait que la moitie du degagement a chaque bout. float lo2 = lo + LampWallCabinetClearance; float hi2 = hi - LampWallCabinetClearance; if (hi2 > lo2) { LengthAvailable = hi2 - lo2; PositionToInstall += Direction * ((lo2 + hi2) * 0.5f); print("[AI] Lampes : cote Avant de " + avant + " -> longueur utile " + LengthAvailable + " apres " + LampWallCabinetClearance + " de degagement A CHAQUE BOUT (au lieu de " + avant + ")."); } else { // Cote Avant trop court pour deux degagements : on garde l'etendue // mesuree plutot que de produire une longueur negative. LengthAvailable = avant; PositionToInstall += Direction * ((lo + hi) * 0.5f); print("[AI] Lampes : cote Avant de " + avant + " trop court pour " + LampWallCabinetClearance + " de degagement aux deux bouts -> " + "etendue gardee telle quelle."); } } } //Set positions PositionToInstall += 6 * Forward; PositionToInstall.y = _G.HEIGHT * 0.5f - 18; // Max 3 lampes ; seulement 2 si l'îlot fait 60" ou moins. int Qty = (LengthAvailable <= 60f) ? 2 : 3; // Marges aux extrémités : longueur/8 pour 3 lampes, longueur/6 pour 2 lampes (îlot <= 60"). // Les lampes sont réparties uniformément entre les deux marges (centrées). float margin = (Qty == 3) ? LengthAvailable / 8f : LengthAvailable / 6f; float span = LengthAvailable - 2f * margin; float stepLen = (Qty > 1) ? span / (Qty - 1) : 0f; Vector3 startPos = PositionToInstall - Direction * (LengthAvailable * 0.5f); for (int i = 0; i < Qty; i++) { Vector3 lampPos = startPos + Direction * (margin + stepLen * i); Set_TemporySC_Deco("LAMP102", "ceil", Size, lampPos, Angle); CreateLamp.AddLamp(); yield return new WaitForSecondsRealtime(0.5f); } } //Add Lamp to Sink (indépendant de l'îlot) // Référence = le CAISSON ÉVIER, pas la fenêtre : GetWindowSinkIndex() renvoie la première // fenêtre d'un mur sélectionné, qui n'est pas forcément celle de l'évier (la lampe se // retrouvait alors ailleurs dans la pièce, ex. devant le frigo). GameObject Sink = GetSinkCabinet(); Vector3 SinkLampPos; float SinkLampAngle = Angle; if (Sink != null) { // Un caisson mural au-dessus de l'évier occupe déjà la place : la lampe suspendue viendrait // buter dedans. Une FENÊTRE au-dessus, elle, laisse la lampe en place (cas classique). if (HasWallCabinetOverSink(Sink)) { print("[AI] Lampe evier : caisson mural au-dessus de " + Sink.name + " -> aucune lampe."); yield break; } SinkLampPos = Sink.transform.position; // centre du caisson évier (déjà à ~12" du mur) SinkLampAngle = AiGet.ObjectAngle(Sink); } else { // Aucun caisson évier trouvé : repli sur la fenêtre de l'évier. int IndexSinkWindow = GetWindowSinkIndex(); GameObject Window = (IndexSinkWindow != 1000) ? Get.o2("SCENE", _G.OBJs[IndexSinkWindow][0]) : null; if (Window == null) { print("[AI] Lampe évier : ni caisson évier ni fenêtre -> aucune lampe."); yield break; } SinkLampPos = Window.transform.position + 12 * -AiGet.FowardbyObject(Window); } SinkLampPos.y = _G.HEIGHT * 0.5f - 18; Set_TemporySC_Deco("LAMP102", "ceil", Size, SinkLampPos, SinkLampAngle); CreateLamp.AddLamp(); yield return null; } // Caisson évier posé sur un MUR (l'îlot a ses propres lampes) : modèle _AI.BASESINK ou type "sink". static GameObject GetSinkCabinet() { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23) continue; if (string.IsNullOrEmpty(o[22]) || o[22] == "floor" || o[22] == "ceil") continue; // "floor" = îlot if (o[4] != "sink" && o[1] != _AI.BASESINK) continue; GameObject go = Get.o2("SCENE", o[0]); if (go != null) return go; } return null; } // Vrai si un CAISSON occupe la bande haute au-dessus du caisson évier. Le test porte sur les // caissons seulement : une fenêtre ou une hotte au-dessus de l'évier ne doit pas priver la lampe. static bool HasWallCabinetOverSink(GameObject sink) { int sinkIdx = Get.GetObjectIndex(sink.name); if (sinkIdx < 0 || _G.OBJs[sinkIdx] == null) return false; string wnum = _G.OBJs[sinkIdx][22]; if (string.IsNullOrEmpty(wnum) || wnum == "floor" || wnum == "ceil") return false; Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; if (!ObjectExtentOnWall(sinkIdx, start, dir, out float s0, out float s1, out _, out _)) return false; for (int i = 0; i < _G.OBJnum; i++) { if (i == sinkIdx || !DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum || o[2] != "Cabinet") continue; if (!ObjectExtentOnWall(i, start, dir, out float a, out float b, out _, out float top)) continue; if (top <= _AI.FloorLine + WallBandBase) continue; // caisson bas : pas au-dessus if (b > s0 + 0.5f && a < s1 - 0.5f) return true; // recouvre l'évier en plan } return false; } //----------------------------FLOOR---------------------------------------------------------------- public void AddFloor() { _G.FLCs = "CERA.3?#FFFFFF?#8C8C8C?#FFFFFF?1-1?0?1?0.9?0.5?1"; CreateFloor.SetFloor(); } //----------------------------OPTIMISATION DES TRAVEES---------------------------------------------- // Largeurs de caissons réellement déclarées par le catalogue, de la plus GRANDE à la plus petite. // singleCab : échelle de PlaceSingleBase / PlaceSingleWallAtEnd (30 -> 9) ; sinon celle du reste // de run (21 -> 9). static List AvailableWidths(bool wallCab, bool singleCab) { var list = new List(); void Add(string model, float wide) { if (!string.IsNullOrEmpty(model)) list.Add(wide); } if (singleCab) { Add(wallCab ? _AI.Wall30 : _AI.Base30, 30f); Add(wallCab ? _AI.Wall27 : _AI.Base27, 27f); list.Add(24f); // _AI.BASE / _AI.WALL : toujours définis } Add(wallCab ? _AI.Wall21 : _AI.Base21, 21f); Add(wallCab ? _AI.Wall18 : _AI.Base18, 18f); Add(wallCab ? _AI.Wall15 : _AI.Base15, 15f); Add(wallCab ? _AI.Wall12 : _AI.Base12, 12f); Add(wallCab ? _AI.Wall9 : _AI.Base9, 9f); return list; } // Perte prévue en remplissant une travée de longueur L. Reproduit fidèlement PlaceBaseRun / // PlaceWallRun : modules de 24" puis un caisson de finition pris dans l'échelle disponible, ou un // caisson unique quand la travée fait moins de 33". Prédire le VRAI algorithme et non un // remplissage idéal : sinon l'optimisation viserait une découpe que la pose ne produira jamais. static float SectionWaste(float L, bool wallCab) { if (L <= 9f) return Mathf.Max(0f, L); // trop court : rien ne se pose if (L < 33f) { foreach (float w in AvailableWidths(wallCab, true)) if (L >= w) return L - w; return L; } float empty = L - Mathf.Floor(L / 24f) * 24f; if (empty < 3f) return empty; if (empty < 6f) return string.IsNullOrEmpty(wallCab ? _AI.Wall27 : _AI.Base27) ? empty : empty - 3f; if (empty < 9f) return string.IsNullOrEmpty(wallCab ? _AI.Wall30 : _AI.Base30) ? empty : empty - 6f; foreach (float w in AvailableWidths(wallCab, false)) if (empty >= w) return empty - w; return empty; } // Décale le four et le frigo d'au plus ±3" pour que les deux travées qu'ils délimitent tombent sur // des largeurs de caissons disponibles. À lancer APRÈS leur pose et AVANT ComputeWallSections, qui // fige les sections. L'appareil ne sort jamais de sa travée : son jeu (AiGet.Gap) est préservé des // deux côtés. IEnumerator CoroutineOptimizeAppliancePositions() { const float MaxShift = 6f; const float Step = 0.5f; int steps = Mathf.RoundToInt(MaxShift / Step); foreach (string wall in WallSelected) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; // Côté du COIN, utilisé pour départager deux décalages de perte totale égale. // Un coin ne compte que si le mur voisin porte lui aussi des caissons, donc s'il est dans // WallSelected. Coin à la FIN -> section de fin ; coin au DÉBUT -> section de début ; // aucun coin -> section de début. // "Coin au début" et "aucun coin" donnant tous deux la section de début, le test se réduit // au coin de FIN. bool cornerAtEnd = WallSelected.Contains(AiGet.NextWallName(wall).Replace("w", "m")); bool priorityIsStart = !cornerAtEnd; // Portion du mur RÉELLEMENT remplissable, via le même SetDeltaPoint que Add_BASECabinetonWall. // Indispensable : le caisson de coin appartient au mur VOISIN (ADN[22] = "w4" pour un coin au // bout de "w3"), il est donc absent de la liste ci-dessous. Sans ce calcul la travée de fin // allait jusqu'au coin brut, sa perte était nulle, et aucun décalage n'était retenu. List wallObstacles = AiGet.AllObjectsOnWall(wall, "All", "All", _AI.FloorLine + 10, _AI.FloorLine); List usable = SetDeltaPoint(wall, start, AiGet.WallEndPoint(wnum), "Base", wallObstacles); float usableStart = Vector3.Dot(usable[0] - start, dir); float usableEnd = Vector3.Dot(usable[1] - start, dir); // Rangée basse : à ce stade il ne reste que les ouvertures, le four et le frigo. var items = new List<(int idx, float x0, float x1)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; if (!ObjectExtentOnWall(i, start, dir, out float x0, out float x1, out float bottom, out _)) continue; if (bottom >= _AI.FloorLine + BaseBandTop) continue; items.Add((i, x0, x1)); } items.Sort((p, q) => p.x0.CompareTo(q.x0)); for (int k = 0; k < items.Count; k++) { string[] o = _G.OBJs[items[k].idx]; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (n4 != "stov" && n4 != "frid") continue; float gap = AiGet.Gap(o[0]); // BLOC = l'appareil + tout ce qui lui est déjà COLLÉ (la colonne posée contre le frigo, // un caisson posé contre le four). Ces objets doivent suivre le décalage, sinon on // ouvrirait un trou entre eux. Le bloc s'étend de proche en proche. var block = new HashSet { k }; int kLeft = k, kRight = k; float blockX0 = items[k].x0, blockX1 = items[k].x1; for (bool grown = true; grown; ) { grown = false; for (int j = 0; j < items.Count; j++) { if (block.Contains(j)) continue; // Un objet rejoint le bloc s'il est COLLÉ d'un côté OU de l'autre. Écrire // "trop loin des deux côtés -> on saute" est faux : un objet éloigné à gauche a // une distance NÉGATIVE à droite, le ET n'était jamais vrai et TOUT le mur // finissait dans le bloc — plus aucun voisin hors bloc, donc plus de marge de // déplacement, et l'appareil restait sur place. float dl = blockX0 - items[j].x1; // >0 si j est à GAUCHE du bloc float dr = items[j].x0 - blockX1; // >0 si j est à DROITE du bloc bool gluedLeft = dl >= -0.5f && dl <= gap + 0.5f; bool gluedRight = dr >= -0.5f && dr <= gap + 0.5f; if (!gluedLeft && !gluedRight) continue; block.Add(j); if (items[j].x0 < blockX0) { blockX0 = items[j].x0; kLeft = j; } if (items[j].x1 > blockX1) { blockX1 = items[j].x1; kRight = j; } grown = true; } } // Jeu à respecter à chaque bout du bloc : celui de l'objet qui s'y trouve (0 pour un // caisson, 1/4" pour le four, 2" pour le frigo). float gapL = AiGet.Gap(_G.OBJs[items[kLeft].idx][0]); float gapR = AiGet.Gap(_G.OBJs[items[kRight].idx][0]); // Bornes de la travée : voisins réels HORS bloc, limites remplissables du mur à défaut // (et non les bouts bruts : la réserve de coin, l'angle obtus et le recul dû à une // ouverture voisine sont déjà pris en compte dans usableStart / usableEnd). // Le jeu du VOISIN est retiré lui aussi : la travée remplissable s'arrête à son bord PLUS // son dégagement, exactement comme AddCabinetBaseToScene construit ses deltas. Sans ça // une fenêtre voisine faisait surestimer la travée de 6" et la perte prévue était fausse. float leftEdge = usableStart, rightEdge = usableEnd; for (int j = 0; j < items.Count; j++) { if (block.Contains(j)) continue; float gapJ = AiGet.Gap(_G.OBJs[items[j].idx][0]); if (items[j].x1 <= blockX0 + 0.5f) leftEdge = Mathf.Max(leftEdge, items[j].x1 + gapJ); else if (items[j].x0 >= blockX1 - 0.5f) rightEdge = Mathf.Min(rightEdge, items[j].x0 - gapJ); } // Critère LEXICOGRAPHIQUE : // 1. perte TOTALE des deux travées — le vrai objectif, l'espace perdu au total ; // 2. perte du côté prioritaire — ne départage que si le total est le même ; // 3. déplacement minimal. // Le côté prioritaire n'est PAS le critère principal : le mur peut avoir un coin à ses // deux bouts, et la travée à optimiser est alors celle qui gaspille, pas celle que la // règle de coin désigne. Le total seul ne suffisait pas non plus : décaler l'appareil // transfère souvent la perte d'un côté à l'autre sans changer la somme, d'où le // départage par le côté du coin. float best = 0f, bestTotal = float.MaxValue, bestPriority = float.MaxValue; for (int s = -steps; s <= steps; s++) { float d = s * Step; float a0 = blockX0 + d, a1 = blockX1 + d; if (a0 - gapL < leftEdge || a1 + gapR > rightEdge) continue; // sortirait de la travée float wasteStart = SectionWaste(a0 - gapL - leftEdge, false); float wasteEnd = SectionWaste(rightEdge - (a1 + gapR), false); float total = wasteStart + wasteEnd; float priority = priorityIsStart ? wasteStart : wasteEnd; bool better; if (total < bestTotal - 0.01f) better = true; else if (total > bestTotal + 0.01f) better = false; else if (priority < bestPriority - 0.01f) better = true; else if (priority > bestPriority + 0.01f) better = false; else better = Mathf.Abs(d) < Mathf.Abs(best); // à perte égale, bouger le moins possible if (better) { bestTotal = total; bestPriority = priority; best = d; } } if (Mathf.Abs(best) < Step * 0.5f) continue; print("[AI] " + o[0] + " sur " + wnum + " decale de " + best + " avec " + block.Count + " objet(s) : perte totale " + bestTotal + " dont cote " + (priorityIsStart ? "debut" : "fin") + "=" + bestPriority + " (travee " + leftEdge + ".." + rightEdge + ")"); foreach (int j in block) { GameObject g = Get.o2("SCENE", _G.OBJs[items[j].idx][0]); if (g == null) continue; g.transform.position += dir * best; DOIT.SaveObjectPositionInData(g.transform); items[j] = (items[j].idx, items[j].x0 + best, items[j].x1 + best); } yield return null; } yield return null; } yield return null; } //----------------------------FRIDGE---------------------------------------------------------------- public void AddFrige() { StartCoroutine(CoroutineAddFrige()); } // notifyDone : voir CoroutineAddStove. IEnumerator CoroutineAddFrige(bool notifyDone = true) { if (WallSelected == null || WallSelected.Count == 0) { WallSelected = new(); _M.PH(33, 0, "ffffff", 1, 1); SceneModeManager.Instance.SetSceneMode(SceneModes.Ai); yield return new WaitUntil(() => WallSelected != null && WallSelected.Count > 0); } // Frigo déjà en scène : idem le four, on garde sa position mais on aligne son modèle. for (int i = 0; i < _G.OBJnum; i++) if (DOIT.exist(i) && _G.OBJs[i][4] == "frid") { if (ApplyAppliancePreference(i)) yield return _waitForSecondsRealtime2; yield break; } const float fridgeW = 30.5f, fridgeH = 68f, fridgeD = 28f; string bestWall = (WallSelected.Count > 1 ? WallSelected[1] : WallSelected[0]).Replace("m", "w"); Vector3 bestCenter = Vector3.zero; float bestSpace = -1f; foreach (string wall in WallSelected) { string wallW = wall.Replace("m", "w"); Vector3 wStart = AiGet.WallStartPoint(wallW); Vector3 wEnd = AiGet.WallEndPoint(wallW); Vector3 wDir = (wEnd - wStart).normalized; float wLen = Vector3.Distance(wStart, wEnd); List obs = AiGet.AllObjectsOnWall(wall, "All", "All", _AI.CeilingLine, _AI.FloorLine); obs.AddRange(GetWindowsOnWall(wall)); List<(float s, float e)> occ = new(); foreach (GameObject o in obs) { Transform pb = o.transform.Find("PBLB"); Transform pr = o.transform.Find("PBRB"); if (pb == null || pr == null) continue; float da = Vector3.Dot(pb.position - wStart, wDir); float db = Vector3.Dot(pr.position - wStart, wDir); occ.Add((Mathf.Min(da, db), Mathf.Max(da, db))); } occ.Sort((a, b) => a.s.CompareTo(b.s)); float prev = 0f; foreach (var (s, e) in occ) { float gap = s - prev; if (gap > bestSpace) { bestSpace = gap; bestWall = wallW; bestCenter = wStart + wDir * (prev + gap * 0.5f); } prev = Mathf.Max(prev, e); } float lastGap = wLen - prev; if (lastGap > bestSpace) { bestSpace = lastGap; bestWall = wallW; bestCenter = wStart + wDir * (prev + lastGap * 0.5f); } } GameObject wallGO = Get.o1("ROOM/" + bestWall); Vector3 pos = bestCenter - wallGO.transform.forward * (fridgeD * 0.5f); pos.y = -_G.HEIGHT * 0.5f + fridgeH * 0.5f; float rotY = _G.WallsAngle[int.Parse(bestWall[1..]) - 1]; string objName = "frid" + _G.OBJnum; _G.OBJs[_G.OBJnum] = new string[100]; string[] C = _G.OBJs[_G.OBJnum]; C[0] = objName; C[1] = GetApliancesModel("FRID101"); C[2] = "Appliance"; C[3] = "UNLOCK"; C[4] = "frid"; C[5] = "fridge"; C[6] = fridgeW.ToString(); C[7] = fridgeH.ToString(); C[8] = fridgeD.ToString(); C[15] = pos.x.ToString(); C[16] = pos.y.ToString(); C[17] = pos.z.ToString(); C[18] = "0"; C[19] = rotY.ToString(); C[20] = "0"; C[21] = "0"; C[22] = bestWall; C[23] = "R"; C[24] = "0"; C[27] = "Untagged"; C[54] = UIT.Global(Header.Splash, _G.G); C[55] = "0,0,0,0"; C[60] = "GLOBAL"; C[89] = objName; SceneModeManager.SelectedName = objName; ConstructFRIDGE.AddFridge(_G.OBJnum); _G.OBJnum++; DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; CheckIfStove(); CheckIfFridge(); if (notifyDone) ButtonDoneToggle.NotifyDone(); } //----------------------------STOVE---------------------------------------------------------------- public void AddStove() { StartCoroutine(CoroutineAddStove()); } // notifyDone = false quand l'appel vient du pipeline AiProcess : NotifyDone() cocherait le toggle // du bouton en attente (Play), pas celui de "Add Stove". IEnumerator CoroutineAddStove(bool notifyDone = true) { if (WallSelected == null || WallSelected.Count == 0) { WallSelected = new(); _M.PH(33, 0, "ffffff", 1, 1); SceneModeManager.Instance.SetSceneMode(SceneModes.Ai); yield return new WaitUntil(() => WallSelected != null && WallSelected.Count > 0); } // Four déjà en scène (fichier chargé, ajout manuel, passe AI précédente) : on ne le replace pas, // mais son MODÈLE doit suivre la préférence de couleur d'appareils. for (int i = 0; i < _G.OBJnum; i++) if (DOIT.exist(i) && _G.OBJs[i][4] == "stov") { if (ApplyAppliancePreference(i)) yield return _waitForSecondsRealtime2; yield break; } const float stoveW = 30.5f, stoveH = 44f, stoveD = 26f; string bestWall = WallSelected[0].Replace("m", "w"); Vector3 bestCenter = Vector3.zero; float bestSpace = -1f; foreach (string wall in WallSelected) { string wallW = wall.Replace("m", "w"); Vector3 wStart = AiGet.WallStartPoint(wallW); Vector3 wEnd = AiGet.WallEndPoint(wallW); Vector3 wDir = (wEnd - wStart).normalized; float wLen = Vector3.Distance(wStart, wEnd); List obs = AiGet.AllObjectsOnWall(wall, "All", "All", _AI.CeilingLine, _AI.FloorLine); obs.AddRange(GetWindowsOnWall(wall)); // Build sorted occupied intervals along wall direction List<(float s, float e)> occ = new(); foreach (GameObject o in obs) { Transform pb = o.transform.Find("PBLB"); Transform pr = o.transform.Find("PBRB"); if (pb == null || pr == null) continue; float da = Vector3.Dot(pb.position - wStart, wDir); float db = Vector3.Dot(pr.position - wStart, wDir); occ.Add((Mathf.Min(da, db), Mathf.Max(da, db))); } occ.Sort((a, b) => a.s.CompareTo(b.s)); // Find largest free gap float prev = 0f; foreach (var (s, e) in occ) { float gap = s - prev; if (gap > bestSpace) { bestSpace = gap; bestWall = wallW; bestCenter = wStart + wDir * (prev + gap * 0.5f); } prev = Mathf.Max(prev, e); } float lastGap = wLen - prev; if (lastGap > bestSpace) { bestSpace = lastGap; bestWall = wallW; bestCenter = wStart + wDir * (prev + lastGap * 0.5f); } } // Place stove at bestCenter, offset from wall surface GameObject wallGO = Get.o1("ROOM/" + bestWall); Vector3 pos = bestCenter - wallGO.transform.forward * (stoveD * 0.5f); pos.y = -_G.HEIGHT * 0.5f + stoveH * 0.5f; float rotY = _G.WallsAngle[int.Parse(bestWall[1..]) - 1]; string objName = "stov" + _G.OBJnum; _G.OBJs[_G.OBJnum] = new string[100]; string[] C = _G.OBJs[_G.OBJnum]; C[0] = objName; C[1] = GetApliancesModel("STOV101"); C[2] = "Appliance"; C[3] = "UNLOCK"; C[4] = "stov"; C[5] = "stove"; C[6] = stoveW.ToString(); C[7] = stoveH.ToString(); C[8] = stoveD.ToString(); C[15] = pos.x.ToString(); C[16] = pos.y.ToString(); C[17] = pos.z.ToString(); C[18] = "0"; C[19] = rotY.ToString(); C[20] = "0"; C[21] = "0"; C[22] = bestWall; C[23] = "R"; C[24] = "0"; C[27] = "Untagged"; C[41] = _G.CounterHeight.ToString(); C[42] = _G.CounterFront.ToString(); C[54] = UIT.Global(Header.Splash, _G.G); C[55] = "0,0,0,0"; C[60] = "GLOBAL"; C[89] = objName; SceneModeManager.SelectedName = objName; ConstructStove.AddStove(_G.OBJnum); _G.OBJnum++; DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; CheckIfStove(); CheckIfFridge(); if (notifyDone) ButtonDoneToggle.NotifyDone(); } //----------------------------WALL & CEILING--------------------------------------------------------------- public void PaintWalls() { StartCoroutine(PaintaWall()); } IEnumerator PaintaWall() { string walllcode = "none,#ffffff,#A6A6A6,#ffffff,0.25-0.25,0,1,0.25,0,0"; for (int i = 0; i < _G.NW; i++) { if (GameObject.Find("w" + (i + 1).ToString())) { GameObject Wall = GameObject.Find("w" + (i + 1).ToString()).transform.gameObject; _G.WallsMaterial[i] = Wall.transform.GetChild(0).GetComponent().material; foreach(Transform s in Wall.transform) { s.GetComponent().material = UIT_MATERIAL.GetMaterial(walllcode); yield return null; } } } yield return RedrawOpening(); } IEnumerator RedrawOpening() { ConstructOpen.RedrawOpen(); //Ceiliing Transform ceil = SceneModeManager.Ceil; string ceilcode = "none,#FFFFFF,#FFFFFF,#FFFFFF,1,0,0.1,0.2,0.3"; foreach (Transform Section in ceil.transform) { Section.GetComponent().material = UIT_MATERIAL.GetMaterial(ceilcode); } yield return null; } //----------------------------CEILING SPOT--------------------------------------------------------------- public void AddSpotCeilling() { StartCoroutine(CoroutineAddSpotCeilling()); } IEnumerator CoroutineAddSpotCeilling() { //print("_G.NWExterior====" + _G.NWExterior); for (int i = 0; i < _G.NWExterior; i++) { GameObject Wall = Get.o2("ROOM", "w" + (i + 1).ToString()); GameObject PreviuosWall = AiGet.PreviousWall(Wall); GameObject NextWall =AiGet.NextWall(Wall); if (Wall != null && PreviuosWall != null) { Vector3 DirectionWall = -GetDirectionbyObject(Wall); Vector3 DirectionPrevious = GetDirectionbyObject(PreviuosWall); Vector3 DirectionNext = GetDirectionbyObject(NextWall); Vector3 WallProject = Wall.transform.position - 36 * AiGet.FowardbyObject(Wall); Vector3 PreviousProject = PreviuosWall.transform.position - 36 * AiGet.FowardbyObject(PreviuosWall); Vector3 NextProject = NextWall.transform.position - 36 * AiGet.FowardbyObject(NextWall); //First Spot Vector3 PositionToInstall = GetLineIntersection(WallProject, DirectionWall, PreviousProject, DirectionPrevious); PositionToInstall.y = _G.HEIGHT * 0.5f + 0.5f; yield return StartCoroutine(CoroutineddSpotFromWall(PositionToInstall)); yield return new WaitForSecondsRealtime(0.1f); //Add Point on that wall Vector3 PositionToInstallNext = GetLineIntersection(NextProject, DirectionNext, WallProject, DirectionWall); PositionToInstallNext.y = _G.HEIGHT * 0.5f + 1; float AvailableDistance = Vector3.Distance(PositionToInstall, PositionToInstallNext); float Qty = Mathf.Floor(AvailableDistance / 48) ; float Space = AvailableDistance / (Qty+1); if (Qty>0 ) { for (int s = 0; s < Qty; s++) { PositionToInstall -= Space * DirectionWall; yield return StartCoroutine(CoroutineddSpotFromWall(PositionToInstall)); yield return new WaitForSecondsRealtime(0.1f); } } } } DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; ButtonDoneToggle.NotifyDone(); } IEnumerator CoroutineddSpotFromWall(Vector3 Position) { Vector3 Size = new(5, 1, 5); Set_TemporySC_Deco("LAMP107", "ceil", Size, Position, 0); CreateLamp.AddLamp(); yield break; } //----------------------------Glass Door--------------------------------------------------------------- public void AddGlassDoor() { StartCoroutine(CoroutineAddGlassDoor()); } // Remplace les caissons muraux par leur variante VITRÉE (GL) quand elle existe (_AI.WallNNGL != ""). // Ex. un caisson _AI.Wall12 devient _AI.Wall12GL. Vaut aussi pour le coin mural (_AI.CW90 -> _AI.CB90GL). // On re-crée le caisson avec le modèle GL (Set_TemporySC lit son motif de porte vitrée), en conservant // position, mur, angle, retournement, lumière (o[40]), porte (o[61]) et panneau de recouvrement (o[81]). IEnumerator CoroutineAddGlassDoor() { // Seulement si l'option "Glass Door" est active pour ce design. if (_G.OPTIONS == null || !_G.OPTIONS.Contains("Glass Door")) yield break; // Modèle mural -> variante GL (uniquement les paires dont la variante GL est définie). var glMap = new Dictionary(); void MapGL(string baseM, string glM) { if (!string.IsNullOrEmpty(baseM) && !string.IsNullOrEmpty(glM) && !glMap.ContainsKey(baseM)) glMap[baseM] = glM; } MapGL(_AI.Wall12, _AI.Wall12GL); MapGL(_AI.Wall15, _AI.Wall15GL); MapGL(_AI.Wall24, _AI.Wall24GL); MapGL(_AI.WALL, _AI.Wall24GL); // WALL (24" standard) partage le modèle de Wall24 MapGL(_AI.CW90, _AI.CB90GL); // coin mural vitré MapGL(_AI.Wall30OS, _AI.Wall30OSGL); // caisson court au-dessus de l'évier if (glMap.Count == 0) { DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; yield break; } // Références pour les caissons DROITS : CUISSON (cuisinière, sinon hotte) ET FENÊTRE (évier). // On vitre les 2 plus proches de CHAQUE référence (<= 42"). Si aucun près de la hotte, // la fenêtre prend le relais. Les COINS sont toujours vitrés, sans condition. const float GLASS_NEAR = 42f; // pouces (distance horizontale max au centre de référence) Vector3 cook = GetStovePosition(); if (cook == Vector3.zero) { for (int k = 0; k < _G.OBJnum; k++) { if (!DOIT.exist(k)) continue; string[] ok = _G.OBJs[k]; if (ok != null && ok.Length > 17 && ok[4] == "hood") { cook = new Vector3(DOIT.ConvertStringToNumber(ok[15]), 0f, DOIT.ConvertStringToNumber(ok[17])); break; } } } Vector2 cook2 = new(cook.x, cook.z); bool hasCook = cook != Vector3.zero; // Référence "évier" : la fenêtre d'évier si elle existe, SINON le caisson évier lui-même. // Sans ce repli, un évier posé hors fenêtre (PlaceSinkOnWidestWallSection) ne vitrait rien // autour de lui : GetWindowSinkIndex rendait 1000 et AddClosest sortait immédiatement. Vector2 win2 = Vector2.zero; bool hasWin = false; int wsi = GetWindowSinkIndex(); if (wsi != 1000 && DOIT.exist(wsi)) { string[] w = _G.OBJs[wsi]; win2 = new Vector2(DOIT.ConvertStringToNumber(w[15]), DOIT.ConvertStringToNumber(w[17])); hasWin = true; } else { GameObject sinkCab = GetSinkCabinet(); if (sinkCab != null) { win2 = new Vector2(sinkCab.transform.position.x, sinkCab.transform.position.z); hasWin = true; } } // Coins (toujours) + candidats droits. Borne _G.OBJnum figée avant toute création. var toGlass = new HashSet(); var candidates = new List<(int idx, float x, float z)>(); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 24) continue; if (o[2] != "Cabinet") continue; if (string.IsNullOrEmpty(o[22]) || o[22] == "floor" || o[22] == "ceil") continue; // muraux seulement if (!glMap.TryGetValue(o[1], out string glm)) continue; if (glm == _AI.CB90GL) { toGlass.Add(i); continue; } // coin : TOUJOURS vitré // Caisson COURT au-dessus de l'évier : TOUJOURS vitré lui aussi, sans passer par la règle // de proximité. Il est posé sur mesure au-dessus de l'évier, sa vocation ne dépend pas // d'un classement de distance — et il en sortait perdant dès qu'un autre caisson mural // était plus près du point de référence. if (IsOverSinkModel(o[1])) { toGlass.Add(i); continue; } candidates.Add((i, DOIT.ConvertStringToNumber(o[15]), DOIT.ConvertStringToNumber(o[17]))); } // Les 2 caissons droits les plus proches d'une référence donnée (<= 42"). void AddClosest(Vector2 refP, bool has) { if (!has) return; var near = new List<(int idx, float d)>(); foreach (var c in candidates) { float d = Vector2.Distance(refP, new Vector2(c.x, c.z)); if (d <= GLASS_NEAR) near.Add((c.idx, d)); } near.Sort((a, b) => a.d.CompareTo(b.d)); for (int k = 0; k < near.Count && k < 2; k++) toGlass.Add(near[k].idx); } AddClosest(cook2, hasCook); // près de la hotte/cuisson AddClosest(win2, hasWin); // près de la fenêtre // REMPLACEMENT. foreach (int i in toGlass) { string[] o = _G.OBJs[i]; if (o == null || string.IsNullOrEmpty(o[0]) || o[0] == "null") continue; if (!glMap.TryGetValue(o[1], out string glModel)) continue; GameObject target = Get.o2("SCENE", o[0]); if (target == null) continue; Vector3 pos = target.transform.position; float angle = AiGet.ObjectAngle(target); string wall = o[22]; string flip = string.IsNullOrEmpty(o[23]) ? "F" : o[23]; string c40 = o[40]; // néon sous caisson string c51 = o[51]; // filler droit (posé par CoroutineAddFillers) string c61 = o[61]; // porte string c81 = o[81]; // panneau de recouvrement (fillers) // Le reste du travail de CoroutineAddFillers. CreateCAB REMET tous ces champs à leur // valeur par défaut et ne lit pas _G.SC pour eux : ils sont réappliqués après coup. string c37 = o[37], c38 = o[38], c50 = o[50], c55 = o[55], c63 = o[63]; bool isCorner = glModel == _AI.CB90GL; DestroyImmediate(target); _G.OBJs[i][0] = "null"; // libère l'ancien slot (évite un caisson fantôme à la sauvegarde) Set_TemporySC(glModel, wall, pos, angle, flip); // La vitre s'affiche via ADN[25] = motif de porte (contient le "g"). On le récupère // du modèle GL dans la librairie des caissons. _G.SC[25] = UIT.Value(Library.Cabinet, glModel, Header.Pattern_Door_1); _G.SC[61] = UIT.Value(Library.Cabinet, glModel, Header.Door_ID); print("For Ai change to cabinet glass ===_G.SC[25]===="+_G.SC[25]+ "and _G.SC[61]===="+_G.SC[61]); if (isCorner) { _G.SC[28] = _AI.WallDepth.ToSafeString(); _G.SC[29] = _AI.WallDepth.ToSafeString(); } if (!string.IsNullOrEmpty(c40)) _G.SC[40] = c40; // lu par CreateCAB (C[40], caissons W) if (!string.IsNullOrEmpty(c61)) _G.SC[61] = c61; // lu par CreateCAB (C[61]) // c51 / c81 ne passent PAS par _G.SC : CreateCAB ne les lit pas -> repris après AddCab(). _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); // CreateCAB écrase C[37],C[38],C[50],C[51],C[55],C[63],C[81] avec les valeurs par défaut // ("0" / "NONE" / "0_0_0_0_0_0_0") et n'en lit AUCUNE depuis _G.SC : sans cette reprise, // le caisson vitré repart sans le filler ni le panneau de recouvrement que // CoroutineAddFillers avait posés (le jeu à côté du coin restait ouvert). int ni = _G.OBJnum - 1; string[] n = _G.OBJs[ni]; bool restored = false; if (HasFillerCode(c50)) { n[50] = c50; n[37] = c37; restored = true; } if (HasFillerCode(c51)) { n[51] = c51; n[38] = c38; restored = true; } if (HasRecoverCode(c81)) { n[81] = c81; restored = true; } if (restored) { if (!string.IsNullOrEmpty(c55)) n[55] = c55; // débord du splash sur le filler if (!string.IsNullOrEmpty(c63)) n[63] = c63; // finition assortie du filler DOIT.REDOCAB(ni); yield return new WaitForSecondsRealtime(0.5f); } yield return null; } DOIT.AllSelOff(); yield return _waitForSecondsRealtime2; } //----------------------------MOLDINGS (Fascia / Valance / Ogee)--------------------------------------- public void AddMoldings() { StartCoroutine(CoroutineAddMoldings()); } // Applique Fascia / Valance / Ogee sur les caissons HAUTS si l'option est dans _G.OPTIONS ET le modèle // _AI existe. CODE = modèle?couleur(porte ADN[61])?6 dans C[66]/C[65]/C[64], comme MoldingPNL.AddToScene, // puis _MOL.SetMolding() construit les moulures sur les groupes de caissons muraux. IEnumerator CoroutineAddMoldings() { if (_G.OPTIONS == null || _G.OPTIONS.Count == 0) yield break; bool doFascia = _G.OPTIONS.Contains("Fascia") && !string.IsNullOrEmpty(_AI.FASCIA); bool doValance = _G.OPTIONS.Contains("Valance") && !string.IsNullOrEmpty(_AI.VALANCE); bool doOgee = _G.OPTIONS.Contains("Ogee") && !string.IsNullOrEmpty(_AI.OGEE); print("doFascia====="+doFascia); print("doValance====="+doValance); print("doOgee ====="+doOgee); if (!doFascia && !doValance && !doOgee) yield break; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 67) continue; if (o[2] != "Cabinet") continue; if (string.IsNullOrEmpty(o[22]) || o[22][0] != 'w') continue; // au mur seulement // Reçoivent les moulures de tête : les caissons MURAUX (posés en hauteur, ADN[21] > 30) // et les COLONNES, qui montent du sol au plafond. Une colonne a ADN[21] = 0 et tombait // donc dans le filtre des caissons bas : elle n'a jamais reçu de fascia ni d'ogee, alors // que ConstructFASCIA et ConstructOGEE traitent explicitement le modèle "T1". bool isTall = o[5] == "TA" || o[4] == "T1"; if (!isTall && DOIT.ConvertStringToNumber(o[21]) <= 30f) continue; // Couleur assortie à la porte (ADN[61] = "DoorID?ColorID"). string[] dp = (o[61] ?? "").Split('?'); string color = dp.Length > 1 ? dp[1] : o[61]; if (doOgee) o[64] = _AI.OGEE + "?" + color + "?4"; if (doValance) o[65] = _AI.VALANCE + "?" + color + "?2"; if (doFascia) o[66] = _AI.FASCIA + "?" + color + "?6"; } _MOL.SetMolding(); // construit Fascia/Ogee/Valance sur les groupes de caissons muraux yield return _waitForSecondsRealtime2; } //----------------------------BACK SPLASH--------------------------------------------------------------- // Dosseret : devant chaque mur à caissons, un GameObject "backsplash{n}" contenant des mesh box // (recette/couleur backsplash GLOBALE) couvrant la largeur des caissons, du plancher jusqu'au TOP // du caisson le plus haut, en soustrayant la partie des fenêtres/portes/patios/openings qui recoupe. public void AddBackSplash() { string CT = UIT.Global(Header.Splash, _G.G); // recette/couleur backsplash globale foreach (string wall in WallSelected) AddBackSplashOnWall(wall, CT); } void AddBackSplashOnWall(string wall, string CT) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wall); Vector3 dir = (AiGet.WallEndPoint(wall) - start).normalized; float floorY = -_G.HEIGHT * 0.5f; float cabMinX = float.MaxValue, cabMaxX = float.MinValue; float cabTop = float.MinValue; // hauteur/sol du TOP du caisson le plus haut var openings = new List<(float x0, float x1, float y0, float y1)>(); // hauteurs relatives au sol for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; GameObject g = Get.o2("SCENE", o[0]); if (g == null) continue; // Travée du bandeau = caissons + STOVE + LAVE-VAISSELLE (le dosseret couvre derrière eux). bool isDish = o[1] == _AI.DISHWASHER || (o[0] != null && o[0].Length >= 4 && o[0][..4] == "dish"); bool isSpanItem = o[2] == "Cabinet" || o[4] == "stov" || isDish; if (isSpanItem) { Transform L = g.transform.Find("PBLB"); Transform R = g.transform.Find("PBRB"); float x0, x1, top; if (L != null && R != null) { float xL = Vector3.Dot(L.position - start, dir); float xR = Vector3.Dot(R.position - start, dir); x0 = Mathf.Min(xL, xR); x1 = Mathf.Max(xL, xR); Transform TU = g.transform.Find("PBLU"); top = (TU != null ? TU.position.y : L.position.y + AiGet.ObjectHeight(g)) - floorY; } else // stove/lave-vaisselle sans coins PBLB -> centre ± dimensions { float cx = Vector3.Dot(g.transform.position - start, dir); float halfW = AiGet.ObjectWide(g) * 0.5f; x0 = cx - halfW; x1 = cx + halfW; top = (g.transform.position.y + AiGet.ObjectHeight(g) * 0.5f) - floorY; } cabMinX = Mathf.Min(cabMinX, x0); cabMaxX = Mathf.Max(cabMaxX, x1); cabTop = Mathf.Max(cabTop, top); } else if (o[2] == "Window" || o[2] == "Door" || o[2] == "Opening" || o[2] == "Patio") { // Cadre EXTÉRIEUR (les coins PBLB/PBLU sont sur le verre, réduits de 4" par la moulure) : // centre ± dimensions nominales -> le bandeau rejoint le cadre sans gap. float cx = Vector3.Dot(g.transform.position - start, dir); float halfW = AiGet.ObjectWide(g) * 0.5f; float halfH = AiGet.ObjectHeight(g) * 0.5f; float cyH = g.transform.position.y - floorY; openings.Add((cx - halfW, cx + halfW, cyH - halfH, cyH + halfH)); } } if (cabMinX > cabMaxX || cabTop <= 2f) return; // aucun caisson sur ce mur // Si un caisson est à <= 6" d'un bord du mur, on étend le bandeau jusqu'au mur (comble le coin). float wallLen = Vector3.Distance(start, AiGet.WallEndPoint(wall)); if (cabMinX <= 6f) cabMinX = 0f; if (wallLen - cabMaxX <= 6f) cabMaxX = wallLen; // Mur SUIVANT : s'il a un caisson à <= 6" du coin, étendre ce bandeau jusqu'au bout du mur // (les deux dosserets se rejoignent au coin sans laisser de mur nu). if (cabMaxX < wallLen && NextWallCabinetNearCorner(wall, AiGet.WallEndPoint(wall), 6f)) cabMaxX = wallLen; float X0 = cabMinX, X1 = cabMaxX, Y0 = 0f, Y1 = cabTop; // Retire les box de dosseret existantes de ce mur (re-génération propre). RemoveSplashOnWall(wnum); float angle = GetAngleByWall(wall); Vector3 forward = -Get.o1(wall).transform.forward.normalized; // Colonnes aux bords des ouvertures ; dans chaque colonne on retire les Y couverts (sous/au-dessus). var xb = new List { X0, X1 }; foreach (var op in openings) { float a = Mathf.Clamp(op.x0, X0, X1), b = Mathf.Clamp(op.x1, X0, X1); if (b - a > 0.5f) { xb.Add(a); xb.Add(b); } } xb.Sort(); for (int c = 0; c < xb.Count - 1; c++) { float xa = xb[c], xc = xb[c + 1]; if (xc - xa < 1f) continue; float xmid = (xa + xc) * 0.5f; var covered = new List<(float y0, float y1)>(); foreach (var op in openings) if (op.x0 <= xmid && op.x1 >= xmid) { float y0 = Mathf.Clamp(op.y0, Y0, Y1), y1 = Mathf.Clamp(op.y1, Y0, Y1); if (y1 - y0 > 0.5f) covered.Add((y0, y1)); } covered.Sort((p, q) => p.y0.CompareTo(q.y0)); float cursor = Y0; foreach (var cv in covered) { if (cv.y0 > cursor + 0.5f) MakeBackSplashBox(wnum, start, dir, forward, angle, xa, xc, cursor, cv.y0, CT); cursor = Mathf.Max(cursor, cv.y1); } if (Y1 > cursor + 0.5f) MakeBackSplashBox(wnum, start, dir, forward, angle, xa, xc, cursor, Y1, CT); } } // Vrai si le mur SUIVANT a un caisson (ou stove/lave-vaisselle) dont un bord est à <= threshold du coin. bool NextWallCabinetNearCorner(string wall, Vector3 corner, float threshold) { string nextW = AiGet.NextWallName(wall); Vector2 c = new(corner.x, corner.z); for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != nextW) continue; bool isDish = o[1] == _AI.DISHWASHER || (o[0] != null && o[0].Length >= 4 && o[0][..4] == "dish"); if (!(o[2] == "Cabinet" || o[4] == "stov" || isDish)) continue; GameObject g = Get.o2("SCENE", o[0]); Transform L = g != null ? g.transform.Find("PBLB") : null; Transform R = g != null ? g.transform.Find("PBRB") : null; if (L == null || R == null) continue; float dL = Vector2.Distance(new Vector2(L.position.x, L.position.z), c); float dR = Vector2.Distance(new Vector2(R.position.x, R.position.z), c); if (Mathf.Min(dL, dR) <= threshold) return true; } return false; } void MakeBackSplashBox(string wnum, Vector3 start, Vector3 dir, Vector3 forward, float angle, float xa, float xc, float ya, float yb, string CT) { float width = xc - xa; float height = yb - ya; if (width < 1f || height < 2f) return; Vector3 pos = start + dir * ((xa + xc) * 0.5f) + forward * 0.5f; pos.y = -_G.HEIGHT * 0.5f + (ya + yb) * 0.5f; // Objet "spla{n}" enregistré dans _G.OBJs pour être SAUVEGARDÉ/RECHARGÉ (voir ConstructSplash). // Reconstruit ensuite via ConstructSplash.AddSplash (cube monde, matériau recette splash globale). string[] C = new string[100]; C[0] = "spla" + _G.OBJnum; C[1] = CT; C[2] = "Splash"; C[3] = "UNLOCK"; C[4] = "splash"; C[5] = "splash"; C[6] = width.ToString(); C[7] = height.ToString(); C[8] = "0.25"; C[15] = pos.x.ToString(); C[16] = pos.y.ToString(); C[17] = pos.z.ToString(); C[18] = "0"; C[19] = angle.ToString(); C[20] = "0"; C[22] = wnum; // mur : utilisé par CameraAround.Hide et IsInsideRoom C[54] = CT; // recette/couleur splash _G.OBJs[_G.OBJnum] = C; ConstructSplash.AddSplash(_G.OBJnum); _G.OBJnum += 1; } // Supprime les box de dosseret (Splash) d'un mur donné + son conteneur (re-génération). void RemoveSplashOnWall(string wnum) { string digits = ""; foreach (char ch in (wnum ?? "")) if (char.IsDigit(ch)) digits += ch; GameObject cont = GameObject.Find("backsplash" + digits); if (cont != null) Destroy(cont); // détruit aussi les box enfants for (int i = 0; i < _G.OBJnum; i++) { string[] o = _G.OBJs[i]; if (o == null || o[0] == "null") continue; if (o[2] == "Splash" && o[22] == wnum) o[0] = "null"; // box déjà détruite avec le conteneur } } //--------------------------OVER SINK----------------------------------------------------------------- // Hauteurs acceptées pour un caisson mural AU-DESSUS d'un évier, par ordre de préférence : on // dégage la tête au lavage, donc on prend le plus haut possible parmi les modèles COURTS. static readonly float[] OverSinkHeights = { 18f, 15f, 12f }; // Modèles "over sink" déclarés par le catalogue. Ils sont déclinés PAR LARGEUR (_AI.Wall24OS = 24", // _AI.Wall30OS = 30") et chaque client n'en a pas forcément : baindepot n'a que le 30", les autres // que le 24" ou rien. Ajouter une largeur au catalogue = ajouter son champ à ce tableau. static string[] OverSinkModels => new[] { _AI.Wall24OS, _AI.Wall30OS }; // Le modèle "over sink" du catalogue dont la largeur correspond, "" si aucun. static string OverSinkModelForWidth(float wide) { foreach (string m in OverSinkModels) { if (string.IsNullOrEmpty(m)) continue; if (Mathf.Abs(UIT.ValueFloat(Library.Cabinet, m, Header.A) - wide) <= 0.5f) return m; } return ""; } // Vrai si ce modèle est un caisson mural "over sink" : sa position est imposée par l'évier, les // passes de justification ne doivent pas le déplacer. static bool IsOverSinkModel(string model) { foreach (string m in OverSinkModels) if (SameModel(model, m)) return true; return false; } // Modèle "over sink" à poser au-dessus d'un évier de largeur sinkWide : le plus large qui ne // déborde pas de l'évier ; à défaut (évier plus étroit que tout le catalogue) le plus étroit. static string ChooseOverSinkModel(float sinkWide) { string best = "", narrowest = ""; float bestW = -1f, narrowestW = float.MaxValue; foreach (string m in OverSinkModels) { if (string.IsNullOrEmpty(m)) continue; float w = UIT.ValueFloat(Library.Cabinet, m, Header.A); if (w <= 0f) continue; if (w < narrowestW) { narrowestW = w; narrowest = m; } if (w <= sinkWide + 0.5f && w > bestW) { bestW = w; best = m; } } return string.IsNullOrEmpty(best) ? narrowest : best; } // Vrai si la travée [x0,x1] du mur est libre dans la bande HAUTE : ni fenêtre, ni hotte, ni colonne, // ni caisson déjà posé ne la recoupe. SectionAllowed ne suffit pas : il ne filtre que les murs // ayant une restriction de sections dans l'UI. static bool HighBandSpanFree(string wall, float x0, float x1) { string wnum = wall.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; if (x0 < 0f || x1 > Vector3.Distance(start, AiGet.WallEndPoint(wnum))) return false; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; if (o[2] == "Splash") continue; if (!ObjectExtentOnWall(i, start, dir, out float a, out float b, out _, out float top)) continue; if (top <= _AI.FloorLine + WallBandBase) continue; // ne monte pas dans la bande haute if (b > x0 + 0.5f && a < x1 - 0.5f) return false; // chevauchement } return true; } // Pose le caisson mural COURT au-dessus de chaque caisson évier mural, AVANT le remplissage des // rangées : sa largeur est alors celle du catalogue (24" ou 30"), pas un reste de rangée. IEnumerator CoroutineAddCabinetOverSink() { int count = _G.OBJnum; // borne figée : la boucle crée des caissons for (int i = 0; i < count; i++) { if (!DOIT.exist(i)) continue; string[] ADN = _G.OBJs[i]; if (ADN[2] != "Cabinet" || ADN[1] != _AI.BASESINK) continue; if (string.IsNullOrEmpty(ADN[22]) || !WallSelected.Contains(ADN[22].Replace("w", "m"))) continue; GameObject sink = Get.o2("SCENE", ADN[0]); if (sink == null) continue; string wall = ADN[22]; string model = ChooseOverSinkModel(AiGet.ObjectWide(sink)); if (string.IsNullOrEmpty(model)) { print("[AI] Evier " + ADN[0] + " : aucun modele over sink dans ce catalogue -> caisson standard"); continue; } float newW = UIT.ValueFloat(Library.Cabinet, model, Header.A); float wallH = UIT.ValueFloat(Library.Cabinet, _AI.WALL, Header.B); float newH = UIT.ValueFloat(Library.Cabinet, model, Header.B); if (newW <= 0f || newH <= 0f || wallH <= 0f) continue; // Centré sur l'évier, ramené à la PROFONDEUR MURALE, et remonté pour que son dessus reste // aligné sur le haut de la rangée murale. Vector3 Forward = -Get.o1(wall).transform.forward.normalized; Vector3 pos = sink.transform.position + ((_AI.WallDepth - _AI.BaseDepth) * 0.5f) * Forward; pos.y = _AI.WallHeightPosition + (wallH - newH) * 0.5f; // Place libre ? (fenêtre au-dessus de l'évier, hotte, colonne, coin mural déjà posé...) Vector3 start = AiGet.WallStartPoint(wall); Vector3 dir = (AiGet.WallEndPoint(wall) - start).normalized; float cx = Vector3.Dot(pos - start, dir); if (!HighBandSpanFree(wall, cx - newW * 0.5f, cx + newW * 0.5f)) { print("[AI] Evier " + ADN[0] + " : travee haute occupee -> pas de caisson au-dessus"); continue; } if (!SectionAllowed(wall, pos, true)) continue; // section desactivee / sous-fenetre print("[AI] Caisson over sink " + model + " (" + newW + "x" + newH + ") pose au-dessus de " + ADN[0]); Set_TemporySC(model, wall, pos, GetAngleByWall(wall), "F"); _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; _G.SC[61] = _AI.Door2; _G.SC[60] = "GLOBAL2"; CreateCAB.AddCab(); yield return _waitForSecondsRealtime2; } yield return null; } public void VerifyWallCabinetOverSink() => StartCoroutine(CoroutineVerifyWallCabinetOverSink()); IEnumerator CoroutineVerifyWallCabinetOverSink() { int count = _G.OBJnum; // borne figée : la boucle recrée des caissons for (int i = 0; i < count; i++) { if (!DOIT.exist(i)) continue; string[] ADN = _G.OBJs[i]; if (ADN[2] != "Cabinet" || ADN[1] != _AI.BASESINK) continue; if (string.IsNullOrEmpty(ADN[22]) || !WallSelected.Contains(ADN[22].Replace("w", "m"))) continue; GameObject Target = Get.o2("SCENE", ADN[0]); if (Target == null) continue; GameObject OverL = GetObjectOver(Target, 6); GameObject OverR = GetObjectOver(Target, -6); bool isCabinetOver = false; if (OverL != null && OverL.name.Contains("cabi")) { yield return StartCoroutine(CoroutineChangeCabinetOverSink(OverL, ADN[22])); ADN[55] = "10,40,0,0"; isCabinetOver = true; } if (OverR != null && OverR.name.Contains("cabi")) { yield return StartCoroutine(CoroutineChangeCabinetOverSink(OverR, ADN[22])); ADN[55] = "10,40,10,0"; isCabinetOver = true; } if (isCabinetOver) DOIT.REDOCAB(i); } yield return null; } IEnumerator CoroutineChangeCabinetOverSink(GameObject Target, string WallNumber) { float TargetWide = AiGet.ObjectWide(Target); float TargetHeight = AiGet.ObjectHeight(Target); string TargetModel = GetObjectADN(Target, 4); // "W1" = caisson mural droit // 1) Le modèle "over sink" DÉCLARÉ par le catalogue pour cette largeur : c'est le choix voulu. // 2) À défaut (largeur non déclarée), un caisson mural de même largeur en 18 -> 15 -> 12 de haut. string NewCab = OverSinkModelForWidth(TargetWide); if (string.IsNullOrEmpty(NewCab)) NewCab = FindShortWallCabinet(TargetWide, TargetModel); if (string.IsNullOrEmpty(NewCab)) yield break; float NewHeight = UIT.ValueFloat(Library.Cabinet, NewCab, Header.B); if (NewHeight <= 0f || NewHeight >= TargetHeight) yield break; // rien de plus court : on garde // Le HAUT reste aligné sur le reste de la rangée -> on remonte de la moitié de la différence. Vector3 PositionToInstall = Target.transform.position; PositionToInstall.y += (TargetHeight - NewHeight) * 0.5f; float angle = AiGet.ObjectAngle(Target); string C61 = GetObjectADN(Target, 61); print("[AI] Caisson sur evier " + Target.name + " : " + GetObjectUniqueName(Target) + " (" + TargetHeight + " haut) -> " + NewCab + " (" + NewHeight + " haut)"); // Libérer l'entrée ADN du caisson remplacé, sinon DOIT.exist() la voit encore et toutes les // passes suivantes (fillers, justification, dosseret, liste de prix) travaillent sur un fantôme. int oldIdx = Get.GetObjectIndex(Target.name); DestroyImmediate(Target); if (oldIdx >= 0 && _G.OBJs[oldIdx] != null) _G.OBJs[oldIdx][0] = "null"; Set_TemporySC(NewCab, WallNumber, PositionToInstall, angle, "F"); _G.SC[61] = C61; _G.SC[40] = "Off_150_F1E6B7?On_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7"; CreateCAB.AddCab(); yield return new WaitForSecondsRealtime(1); } // Cherche dans la librairie du chemin courant un caisson mural de la largeur demandée, du même // modèle (W1...) et d'une des hauteurs OverSinkHeights, en respectant cet ordre de préférence. // Passe par la librairie plutôt que par des champs _AI figés : chaque catalogue a ses propres // codes, et les hauteurs courtes ne sont pas les mêmes d'un fournisseur à l'autre. static string FindShortWallCabinet(float wide, string model) { List lib = UIT.GetLibrary(Library.Cabinet); if (lib == null || lib.Count < 2 || wide <= 0f) return ""; int iA = UIT.Ind(Library.Cabinet, Header.A); // largeur int iB = UIT.Ind(Library.Cabinet, Header.B); // hauteur int iCat = UIT.Ind(Library.Cabinet, Header.Category); // "WA" int iMod = UIT.Ind(Library.Cabinet, Header.Model); // "W1" int need = Mathf.Max(Mathf.Max(iA, iB), Mathf.Max(iCat, iMod)); foreach (float h in OverSinkHeights) { for (int i = 1; i < lib.Count; i++) { if (string.IsNullOrEmpty(lib[i])) continue; string[] l = lib[i].Split(','); if (l.Length <= need) continue; if (l[iCat] != "WA") continue; if (!string.IsNullOrEmpty(model) && l[iMod] != model) continue; if (Mathf.Abs(DOIT.ConvertStringToNumber(l[iA]) - wide) > 0.5f) continue; if (Mathf.Abs(DOIT.ConvertStringToNumber(l[iB]) - h) > 0.5f) continue; return l[0]; // colonne 0 = clé utilisée par UIT.GetLibraryLine / Set_TemporySC } } return ""; } //----------------------------SETTING AND DELETE---------------------------------------------------------------- // "R" si Pos est à DROITE de la référence la plus proche du mur — four, évier ou frigo —, "L" // sinon. "" quand le mur n'en porte aucune : l'appelant garde alors la valeur qu'il avait. // "Droite" = sens des x CROISSANTS le long du mur (début -> fin), la convention utilisée partout // ailleurs pour ADN[23]. static string HandleSideFromReferences(string WallNumber, Vector3 Pos, out string refName, out float cx, out float rxBest) { refName = ""; cx = 0f; rxBest = 0f; if (string.IsNullOrEmpty(WallNumber) || WallNumber == "floor" || WallNumber == "ceil") return ""; string wnum = WallNumber.Replace("m", "w"); Vector3 start = AiGet.WallStartPoint(wnum); Vector3 dir = (AiGet.WallEndPoint(wnum) - start).normalized; cx = Vector3.Dot(Pos - start, dir); float best = float.MaxValue; string side = ""; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != wnum) continue; string n4 = o[0] != null && o[0].Length >= 4 ? o[0][..4] : ""; if (n4 != "stov" && n4 != "frid" && o[4] != "sink" && !SameModel(o[1], _AI.BASESINK)) continue; GameObject g = Get.o2("SCENE", o[0]); if (g == null) continue; float rx = Vector3.Dot(g.transform.position - start, dir); float d = Mathf.Abs(cx - rx); if (d < best) { best = d; rxBest = rx; refName = o[0]; side = cx > rx ? "R" : "L"; } } return side; } public static void Set_TemporySC(string Cabinet_UniqueName, string WallNumber, Vector3 Pos, float angle, string Flip) { print("PositionToInstall=====2=" + Cabinet_UniqueName); _G.SC = new string[101]; _G.SC[0] = Cabinet_UniqueName; _G.SC[4] = UIT.Value(Library.Cabinet, _G.SC[0], Header.Model);//B1 _G.SC[5] = UIT.Value(Library.Cabinet, _G.SC[0], Header.Category);//C_AI.BASE _G.SC[6] = UIT.Value(Library.Cabinet, _G.SC[0], Header.A); _G.SC[7] = UIT.Value(Library.Cabinet, _G.SC[0], Header.B); _G.SC[8] = UIT.Value(Library.Cabinet, _G.SC[0], Header.C); _G.SC[10] = UIT.Value(Library.Cabinet, _G.SC[0], Header.K);//Kick Height _G.SC[11] = "0";//Interiorfinish on/off _G.SC[12] = "0";//Underfinish on/off _G.SC[14] = _G.caCH.ToString();//Counter Height _G.SC[15] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.x); //Pos.x.ToString(); _G.SC[16] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.y); _G.SC[17] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.z); _G.SC[19] = angle.ToString(); _G.SC[21] = UIT.Value(Library.Cabinet, _G.SC[0], Header.FF); _G.SC[22] = WallNumber.Replace("m", "w"); // Côté d'ouverture, décidé DÈS LA CONSTRUCTION. // La librairie déclare "F" pour un modèle à deux portes : aucun côté à choisir, on garde tel // quel. Sinon le caisson a une poignée, et on l'oriente selon la référence la plus proche de // son mur — four, évier ou frigo : "R" s'il est à sa DROITE, "L" sinon. // Le faire ici plutôt qu'en passe séparée évite les retournements après coup : tout // remplacement (porte vitrée, tiroirs, caisson sur évier) repasse par Set_TemporySC et hérite // du bon sens, au lieu de conserver celui du caisson qu'il remplace. _G.SC[23] = Flip; // Trim : une colonne CSV peut ramener " L" ou "L\r", qui ne matchait alors ni "L" ni "R" et // laissait le caisson en "F". string libFlip = (UIT.Value(Library.Cabinet, _G.SC[0], Header.Flip) ?? "").Trim(); // "T1" = les COLONNES. Elles étaient absentes de ce test : posées avec Flip="F" par // Add_TallCabinetonWall, elles gardaient le sens par défaut du modèle au lieu de tourner // leur poignée vers la référence, d'où des poignées du mauvais côté. Les colonnes dont la // librairie annonce Flip="F" (double porte, garde-manger symétrique) restent écartées par // le premier test, comme les caissons de base. if (libFlip != "F" && (_G.SC[4] == "B1" || _G.SC[4] == "W1" || _G.SC[4] == "T1")) { string side = HandleSideFromReferences(WallNumber, Pos, out string refName, out float cx, out float rx); if (!string.IsNullOrEmpty(side)) _G.SC[23] = side; else if (libFlip == "L" || libFlip == "R") _G.SC[23] = libFlip; print("[AI] Cote " + Cabinet_UniqueName + " sur " + WallNumber + " : libFlip='" + libFlip + "' ref=" + (string.IsNullOrEmpty(refName) ? "AUCUNE" : refName) + " cx=" + cx + " rx=" + rx + " modele=" + _G.SC[4] + " -> ADN[23]=" + _G.SC[23]); } print("PositionToInstall=====3=" + _G.SC[17]); _G.CODEPATERN = UIT.Value(Library.Cabinet, _G.SC[0], Header.Pattern_Door_1); _G.CODEPATERN2 = UIT.Value(Library.Cabinet, _G.SC[0], Header.Pattern_Door_2); _G.SC[28] = _AI.BaseDepth.ToSafeString(); _G.SC[29] = _AI.BaseDepth.ToSafeString(); if (Cabinet_UniqueName == _AI.CW90) { _G.SC[28] = _AI.WallDepth.ToSafeString(); _G.SC[29] = _AI.WallDepth.ToSafeString(); } _G.SC[60] = "GLOBAL1"; _G.SC[61] = UIT.Global(Header.Door,_G.G);//Door on/off _G.SC[78] = "Std";//Sides _G.SC[34] = UIT.Value(Library.Cabinet, _G.SC[0], Header.K_Code); _G.SC[55] = "0,0,0,0"; _G.SC[73] = "LEGSKICK"; _G.SC[100] = "FromAI"; } public static void Set_TemporySC_Panel(string Panel_UniqueName, string WallNumber, Vector3 size, Vector3 Pos, float angle, string Flip) { _G.SC = new string[101]; _G.SC[0] = Panel_UniqueName; _G.SC[1] = DOIT.ConvertNumberToStringWithoutSymbol(size.x);// _G.SC[2] = DOIT.ConvertNumberToStringWithoutSymbol(size.y);// _G.SC[3] = DOIT.ConvertNumberToStringWithoutSymbol(size.z);// _G.SC[15] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.x); //Pos.x.ToString(); _G.SC[16] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.y); _G.SC[17] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.z); _G.SC[19] = angle.ToString(); _G.SC[22] = WallNumber.Replace("m", "w"); _G.SC[60] = "GLOBAL" + _G.G.ToString(); _G.SC[61] = "1" + "?" + "0" + "?" + "0" + "?" + "0" + "?" + "0.5" + "?" + "0" + "?" + "0" + "?" + "0";//Color? Rotation? eh? ev? t? ct? ts? slat ? woodSense _G.SC[100] = "FromAI"; } public static void Set_TemporySC_Hood(string Hood_UniqueName, string WallNumber, Vector3 size, Vector3 Pos, float angle) { _G.SC = new string[101]; _G.SC[0] = Hood_UniqueName; _G.SC[1] = DOIT.ConvertNumberToStringWithoutSymbol(size.x);// _G.SC[2] = DOIT.ConvertNumberToStringWithoutSymbol(size.y);// _G.SC[3] = DOIT.ConvertNumberToStringWithoutSymbol(size.z);// _G.SC[15] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.x); //Pos.x.ToString(); _G.SC[16] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.y); _G.SC[17] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.z); _G.SC[19] = angle.ToString(); _G.SC[22] = WallNumber.Replace("m", "w"); string ColorHex=ColorHEX(_G.AI_PREFERENCES["ApliancesColor"]); _G.SC[61]=ColorHex+"?Glossy?none"; _G.SC[100] = "FromAI"; } public static void Set_TemporySC_Dishwasher(string Diswasher_UniqueName, string WallNumber, Vector3 size, Vector3 Pos, float angle) { _G.SC = new string[101]; _G.SC[0] = Diswasher_UniqueName; _G.SC[1] = DOIT.ConvertNumberToStringWithoutSymbol(size.x);// _G.SC[2] = DOIT.ConvertNumberToStringWithoutSymbol(size.y);// _G.SC[3] = DOIT.ConvertNumberToStringWithoutSymbol(size.z);// _G.SC[15] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.x); //Pos.x.ToString(); _G.SC[16] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.y); _G.SC[17] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.z); _G.SC[19] = angle.ToString(); _G.SC[22] = WallNumber.Replace("m", "w"); _G.SC[100] = "FromAI"; } public static void Set_TemporySC_Deco(string Deco_UniqueName, string WallNumber, Vector3 size, Vector3 Pos, float angle) { _G.SC = new string[101]; _G.SC[0] = Deco_UniqueName; _G.SC[1] = DOIT.ConvertNumberToStringWithoutSymbol(size.x);// _G.SC[2] = DOIT.ConvertNumberToStringWithoutSymbol(size.y);// _G.SC[3] = DOIT.ConvertNumberToStringWithoutSymbol(size.z);// string ColorHex = ColorHEX(_G.AI_PREFERENCES["SinkFaucetColor"]); _G.SC[60] = ColorHex+"_Gloss_none";// _G.SC[61] = ColorHex+"_Mat_none";// _G.SC[15] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.x); //Pos.x.ToString(); _G.SC[16] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.y); _G.SC[17] = DOIT.ConvertNumberToStringWithoutSymbol(Pos.z); _G.SC[19] = angle.ToString(); _G.SC[22] = WallNumber.Replace("m", "w"); _G.SC[100] = "FromAI"; } public static void DeleteAllCab() { //string wall = WallSelected[0].Replace("m", "w"); //GameObject SCENE = Get.o1("SCENE"); CameraAround.Show(); for (int i = 0; i < _G.OBJnum; i++) { if (DOIT.exist(i)) { if (new List { "cabi", "pane", "dish", "lamp", "stoo", "hood", "micr", "spla" }.Contains(_G.OBJs[i][0][..4])) { string ObjecName = _G.OBJs[i][0]; _G.OBJs[i][0] = "null"; Destroy(Get.o2("SCENE",ObjecName)); } } } // Les materiaux instancies par face partent avec leurs objets (MAT_TRASH.OnDestroy). // La trace permet de verifier que la memoire redescend au lieu de monter en escalier : // "encore suivis" doit revenir a peu pres au meme niveau d'une generation a l'autre. print("[AI] DeleteAllCab : " + MAT_TRASH.TrackedCount + " materiaux encore suivis, " + MAT_TRASH.FreedCount + " liberes depuis le lancement."); } public static void ShowWallSelected() { GameObject NewWallSelected = Get.o2("Panel_SCENE/PNL/AI_PNL", "WallSelected"); NewWallSelected .GetComponent().text = ""; foreach (string Wall in AI.WallSelected) { //print("Wall selected=====" + Wall); NewWallSelected.GetComponent().text += Wall + ","; } } // Une ouverture à moins de CornerOpeningRange du coin gêne le mur voisin : son encadrement, et le // débattement d'une porte, empiètent dessus. On y recule la fin de la section des caissons. const float CornerOpeningRange = 30f; const float CornerOpeningSetback = 12f; // Vrai s'il y a une ouverture sur le mur VOISIN à moins de 30" du coin partagé avec ce mur. // atEnd = coin de FIN (mur suivant) ; sinon coin de DÉBUT (mur précédent). // Une FENÊTRE ne compte que pour les caissons HAUTS et les COLONNES / garde-manger : un caisson // BAS passe sous l'appui, alors qu'un haut ou une colonne viendrait la recouvrir. // Category : "Base" (fenêtres ignorées), "Wall" ou "Tall" (fenêtres comptées). static bool OpeningNearWallCorner(string wall, string Category, bool atEnd) { string neighbour = atEnd ? AiGet.NextWallName(wall) : AiGet.PreviousWallName(wall); if (string.IsNullOrEmpty(neighbour)) return false; string nw = neighbour.Replace("m", "w"); Vector3 nStart = AiGet.WallStartPoint(nw); Vector3 nDir = (AiGet.WallEndPoint(nw) - nStart).normalized; // Coin partagé = FIN ou DÉBUT de CE mur, projeté sur l'axe du mur voisin (peu importe de quel // bout du mur voisin il s'agit). Vector3 cornerV = atEnd ? AiGet.WallEndPoint(wall) : AiGet.WallStartPoint(wall); float cornerD = Vector3.Dot(cornerV - nStart, nDir); bool windowsCount = Category != "Base"; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; string[] o = _G.OBJs[i]; if (o == null || o.Length < 23 || o[22] != nw) continue; bool isWindow = o[2] == "Window"; bool isGap = o[2] == "Door" || o[2] == "Opening" || o[2] == "Patio"; if (!isGap && !(isWindow && windowsCount)) continue; if (!ObjectExtentOnWall(i, nStart, nDir, out float x0, out float x1, out _, out _)) continue; // Distance du bord le plus proche au coin ; 0 si le coin tombe dans l'emprise. float dist = Mathf.Max(0f, Mathf.Max(x0 - cornerD, cornerD - x1)); if (dist < CornerOpeningRange) { print("[AI] " + wall + " (" + Category + ") : " + o[0] + " a " + dist + " du coin sur " + nw + " -> " + (atEnd ? "fin" : "debut") + " recule de " + CornerOpeningSetback); return true; } } return false; } static bool OpeningNearNextWallCorner(string wall, string Category) => OpeningNearWallCorner(wall, Category, true); static bool OpeningNearPreviousWallCorner(string wall, string Category) => OpeningNearWallCorner(wall, Category, false); public static List SetDeltaPoint(string Wallstring, Vector3 StartPoint, Vector3 EndPoint, string Category, List Obstacles) { float TopLine = _AI.FloorLine + 64; float ButtonLine = _AI.FloorLine; Vector3 Direction = (EndPoint - StartPoint).normalized; float DeltaLength = Vector3.Distance(StartPoint, EndPoint); Vector3 WallStartPoint = StartPoint; Vector3 WallEndPoint = EndPoint; if (DeltaLength < 24) { return new() { StartPoint, EndPoint }; } print("Direction=========================" + Direction); if (Category == "Wall") { TopLine = _AI.CeilingLine; ButtonLine = _AI.FloorLine + 30; } //Check start Angle string PreviousWallName=AiGet.PreviousWallName(Wallstring); float StartAngle = AiGet.AngleBetweenWallAndPrevious(Wallstring, PreviousWallName); if (StartAngle >= 100) { StartPoint += 12 * Direction; } //Check end Angle string NextWallName=AiGet.NextWallName(Wallstring); float EndAngle = AiGet.AngleBetweenWallAndNext(Wallstring,NextWallName); if (EndAngle >= 100) { EndPoint -= 12 * Direction; } //Check on Adjacent Wall // Grâce à WallsFillOrder (mur suivant rempli d'abord), CheckNextWall trouve le caisson au début // du mur suivant s'il existe -> dégagement précis (rien à reculer si ce début est une fenêtre). NextObject = AiGet.CheckNextWall(Wallstring, TopLine, ButtonLine); if (NextObject != null) print("NextObject name===" + NextObject); PreviousObject = AiGet.CheckPreviousWall(Wallstring, TopLine, ButtonLine); if (NextObject != null && !string.IsNullOrEmpty(NextObject.name) && EndAngle < 100) EndPoint -= GetObjectSpaceFromNextorPrevious(NextObject.name, Category) * Direction; if (PreviousObject != null && !string.IsNullOrEmpty(PreviousObject.name) && StartAngle < 100) StartPoint += GetObjectSpaceFromNextorPrevious(PreviousObject.name, Category) * Direction; // Ouverture proche du coin sur le mur voisin -> ce bout-là recule de 12". Contrainte de // dégagement AU COIN, comme GetObjectSpaceFromNextorPrevious ci-dessus : on garde le plus // contraignant des deux au lieu de les additionner. // Les DEUX bouts sont traités : un mur sans coin à son début commence à ras du mur voisin, et // une porte proche de ce coin-là empiétait sur le premier caisson. if (OpeningNearNextWallCorner(Wallstring, Category)) { float already = Vector3.Distance(WallEndPoint, EndPoint); if (already < CornerOpeningSetback) EndPoint -= (CornerOpeningSetback - already) * Direction; } if (OpeningNearPreviousWallCorner(Wallstring, Category)) { float already = Vector3.Distance(WallStartPoint, StartPoint); if (already < CornerOpeningSetback) StartPoint += (CornerOpeningSetback - already) * Direction; } //Check if Obstacle is inDelta from start if (Obstacles.Count > 0) { Vector3 FirstObstacalePointLeft = PointAlign(Obstacles[0].transform.Find("PBLB").transform.position, _AI.FloorLine); Vector3 FirstObstacalePointRight = PointAlign(Obstacles[0].transform.Find("PBRB").transform.position, _AI.FloorLine); float Distance_RealStart_StartPoint = Vector3.Distance(WallStartPoint, StartPoint); float Distance_RealStart_FirstObstacalePointLeft = Vector3.Distance(WallStartPoint, FirstObstacalePointLeft); if (Distance_RealStart_StartPoint > Distance_RealStart_FirstObstacalePointLeft) { StartPoint = FirstObstacalePointLeft; } //if (Distance_RealStart_FirstObstacalePointLeft < 6) { StartPoint = FirstObstacalePointRight; } Vector3 LastObstacalePointLeft = PointAlign(Obstacles[^1].transform.Find("PBLB").transform.position, _AI.FloorLine); Vector3 LastObstacalePointRight = PointAlign(Obstacles[^1].transform.Find("PBRB").transform.position, _AI.FloorLine); float Distance_RealEnd_EndPoint = Vector3.Distance(WallEndPoint, EndPoint); float Distance_RealEnd_LastObstacalePointRight = Vector3.Distance(WallEndPoint, LastObstacalePointRight); if (Distance_RealEnd_EndPoint > Distance_RealEnd_LastObstacalePointRight) { EndPoint = LastObstacalePointRight; } } return new() { StartPoint, EndPoint }; } public static Vector3 PointAlign(Vector3 Point, float Hight) { Vector3 NewPoint = new(Point.x, Hight, Point.z); return NewPoint; } public static void AllMeshDeselect() { DOIT.ResetRoomSelect(); WallSelected.Clear(); } public static void SetGlobals() { //UIT.GlobalSet(Header.Counter, "COUN.9?737373?484848?F6F6F6?1-1?0?1?0.98?0.3?1", _G.G); //_AI.Door1 = "6?16"; //_AI.Door2 = "5?15"; } public static List ReorderWallString(List list) { // Trie en extrayant la partie numérique de chaque string list = list.OrderBy(s => { string numberPart = new string(s.SkipWhile(c => !char.IsDigit(c)).ToArray()); return int.Parse(numberPart); }).ToList(); // Affichage pour test foreach (var item in list) { Console.WriteLine(item); } return list; } //-------------------------------GET------------------------------------------------------------------ public static float GetAngleByWall(string WallNumber) { Vector3 StartPoint = AiGet.WallStartPoint(WallNumber); Vector3 EndPoint = AiGet.WallEndPoint(WallNumber); List Delta = new() { StartPoint, EndPoint }; return GetAngleFromXClockwise(Delta[0], Delta[1]); } static string GetObjectADN(GameObject Object,int IndexADN) { int ADN = Get.GetObjectIndex(Object.name); return _G.OBJs[ADN][IndexADN]; } static string GetObjectUniqueName(GameObject Object) { print("Object name ====" + Object.name); int ADN = Get.GetObjectIndex(Object.name); return _G.OBJs[ADN][1]; } public static float GetAngleFromXClockwise(Vector3 fromPos, Vector3 toPos) { // Direction from A to B in XZ plane Vector2 dir = new Vector2(toPos.x - fromPos.x, toPos.z - fromPos.z).normalized; // Get angle from +X axis (atan2 returns counter-clockwise) float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; // Convert to clockwise angle in [0, 360) float clockwiseAngle = (360 - angle + 360) % 360; return clockwiseAngle; } public static float GetObjectSpaceFromNextorPrevious(string NameObject, string Category) { int ADN = Get.GetObjectIndex(NameObject); string model = _G.OBJs[ADN][1]; string type = _G.OBJs[ADN][4]; // Porte simple (B1/W1) ou évier au coin : dégagement pour OUVRIR les portes. // 48" pour le bas, 36" pour le haut (24 profondeur voisin + ~24/12 débattement porte). if (type == "B1" || type == "W1" || model == _AI.BASESINK) return (Category == "Wall") ? 36f : 48f; if (model == _AI.CB90 || (!string.IsNullOrEmpty(_AI.CB9036) && model == _AI.CB9036)) { return 36; } else if (model == _AI.CBDEADR) { return 24; } else if (model == _AI.CBDEADL) { return 24; } else if (model == _AI.CWDEADR) { return 24; } else if (model == _AI.CWDEADL) { return 24; } else if (model == _AI.BASE) { return 24 + 12; } else if (model == _AI.WALL) { return 24 + 12; } else if (model == _AI.CW90) { return 24; } return 0; } public static GameObject FindFarthestFromPoint(string Wallstring, string Category) //List objects, Vector3 referencePoint) { List ListCabinetOnWall = new(); if (Category == "B1") { ListCabinetOnWall = AiGet.AllObjectsOnWall(Wallstring, "Cabinet", Category, _AI.FloorLine + _AI.CabinetHeight + 1, _AI.FloorLine); } if (Category == "W1") { ListCabinetOnWall = AiGet.AllObjectsOnWall(Wallstring, "Cabinet", Category, _AI.CeilingLine, _AI.FloorLine + _AI.CabinetHeight + 1); } int IndexWall = int.Parse(Wallstring[1..]) - 1; Vector3 StartPoint = _G.ExteriorWallPoints[IndexWall][0]; ListCabinetOnWall.Sort((obj1, obj2) => { float sqrDist1 = (obj1.transform.position - StartPoint).sqrMagnitude; float sqrDist2 = (obj2.transform.position - StartPoint).sqrMagnitude; return sqrDist1.CompareTo(sqrDist2); }); Vector3 PointFrom; if (Wallstring.Replace("w", "m") == WallSelected[0]) { PointFrom = AiGet.WallStartPoint(Wallstring); } else { PointFrom = AiGet.WallEndPoint(Wallstring); } GameObject farthest = null; float maxDistanceSqr = float.MinValue; foreach (GameObject obj in ListCabinetOnWall) { if (obj == null) continue; float Wide = AiGet.ObjectWide(obj); float distSqr = (obj.transform.position - PointFrom).sqrMagnitude; if (distSqr > maxDistanceSqr && Wide == 24) { maxDistanceSqr = distSqr; farthest = obj; } } return farthest; } public static List GetAllObjectsOnFloor(string Wallnunber, string Category, string Model, float UpLine, float downLine, string Sorting) { List Obstacles = new(); bool isUnderline = false;//36 //print("UpLine=====" + UpLine + " downLine=====" + downLine); List Modellist = new(); if (Model == "CornerBASE") { Model = "B2"; Modellist = new() { "B2", "B3", "B4", "B5", "B9", "B10" }; } if (Model == "CornerWall") { Model = "W2"; Modellist = new() { "W2", "W3", "W4", "W5", "W9", "W10" }; } if (Model == "dish") { Model = "dish"; Modellist = new() { "dish"}; } if (Model == "Base_Dish") { Model = "dish"; Modellist = new() { "B1","dish"}; } if (Model == "Cabinet") { Model = "dish"; Modellist = new() { "B1","dish"}; } for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][0].Length >= 4 && (_G.OBJs[i][0][..4] == "lamp" || _G.OBJs[i][0][..4] == "stoo" || _G.OBJs[i][0][..4] == "micr")) continue; if (_G.OBJs[i][22] == Wallnunber && _G.OBJs[i][2] == Category || _G.OBJs[i][22] == Wallnunber && Category == "All" && _G.OBJs[i][2] != "Stool"|| _G.OBJs[i][22] =="floor" && _G.OBJs[i][4]==Model ) { print("From GetAllObjectsOnFloor ===1=" + _G.OBJs[i][0]); GameObject Target = Get.o2("SCENE", _G.OBJs[i][0]); float LineBASE = Target.transform.Find("PBLB").transform.position.y; float LineTop = Target.transform.Find("PBLU").transform.position.y; if (LineBASE <= UpLine && LineBASE >= downLine || LineTop <= UpLine && LineTop >= downLine) { isUnderline = true; } if (_G.OBJs[i][4] == Model || Model == "All" || Modellist.Contains(_G.OBJs[i][4])) { print("From GetAllObjectsOnFloor ===2=" + _G.OBJs[i][0]); print("From GetAllObjectsOnFloor ===3=" + isUnderline); if (isUnderline) { Obstacles.Add(Get.o2("SCENE", _G.OBJs[i][0])); isUnderline = false; } } } } // Obstacles.Sort((obj1, obj2) => // { // float sqrDist1 = (obj1.transform.position - StartPoint).sqrMagnitude; // float sqrDist2 = (obj2.transform.position - StartPoint).sqrMagnitude; // return sqrDist1.CompareTo(sqrDist2); // }); // if (Sorting == "From End") Obstacles.Reverse(); return Obstacles; } public static GameObject GetObjectSide(GameObject Target, List List) { GameObject ObjectSide = null; Vector3 TargetPosition = Target.transform.position; float Distance; //print("List count=====" + List.Count); if (List.Count > 0) { foreach (GameObject Object in List) { Vector3 ObjectPosition = Object.transform.position; Distance = Vector3.Distance(TargetPosition, ObjectPosition); if (Distance <= 30) { ObjectSide = Object; } } } return ObjectSide; } public static string GetWallNumberbyObjectName(string Objectname) { int ADN = Get.GetObjectIndex(Objectname); return _G.OBJs[ADN][22]; } public static GameObject GetObject(string Category, string Wallnumber) { for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (_G.OBJs[i][22] == Wallnumber.Replace("m", "w") && _G.OBJs[i][4] == Category) { return Get.o2("SCENE", _G.OBJs[i][0]); } } return null; } public static Vector3 GetDirectionbyObject(GameObject Target) { return Target.transform.right.normalized; } // Modèle de caisson à TIROIRS pour cette largeur, "" si le catalogue n'en déclare pas. // ARRONDI obligatoire : comparer un float à des littéraux entiers se fait à l'ÉGALITÉ EXACTE, et // ADN[6] passe par DOIT.CNs/CSs — 23.999... ne serait tombé dans aucun cas. static string DrawerModelForWidth(float wide) => Mathf.RoundToInt(wide) switch { 12 => _AI.Drawer12, 15 => _AI.Drawer15, 18 => _AI.Drawer18, 21 => _AI.Drawer21, 24 => _AI.Drawer24, 27 => _AI.Drawer27, 30 => _AI.Drawer30, 33 => _AI.Drawer33, 36 => _AI.Drawer36, _ => "", }; static string CabinetDrawerMatchSizeUniqueName(GameObject Target) { float TargetWide = AiGet.ObjectWide(Target); string model = DrawerModelForWidth(TargetWide); if (string.IsNullOrEmpty(model)) print("[AI] Tiroir : aucun modele pour " + Target.name + " large de " + TargetWide + " (modele actuel " + GetObjectUniqueName(Target) + ")"); return model; } static Vector3 GetCenterPoint(List points) { if (points == null || points.Count == 0) { Debug.LogWarning("La liste est vide."); return Vector3.zero; } Vector3 total = Vector3.zero; foreach (Vector3 point in points) { total += point; } return total / points.Count; } static float GetLeftTouch(GameObject Object) { Vector3 origin = Object.transform.position; Vector3 Direction = -GetDirectionbyObject(Object); float Wide = AiGet.ObjectWide(Object); origin += Wide * 0.5f * Direction; // Perform raycast if (Physics.Raycast(origin, Direction, out RaycastHit hit, 24)) { // Return distance to the hit object return hit.distance+0.5f; } // If nothing hit, return -1 or max distance return -1f; } static float GetRightTouch(GameObject Object) { Vector3 origin = Object.transform.position; Vector3 Direction = GetDirectionbyObject(Object); float Wide = AiGet.ObjectWide(Object); origin += Wide * 0.5f * Direction; // Perform raycast if (Physics.Raycast(origin, Direction, out RaycastHit hit, 24)) { // Return distance to the hit object return hit.distance+0.5f; } // If nothing hit, return -1 or max distance return -1f; } static Vector3 GetIslandCenterPoint(List ListObjectsOnIsland ) { List IslandCabPositions = new(); foreach (GameObject Object in ListObjectsOnIsland) { IslandCabPositions.Add(Object.transform.Find("PBLB").transform.position); IslandCabPositions.Add(Object.transform.Find("PBRB").transform.position); } return GetCenterPoint(IslandCabPositions); } static float GetIslandWide(List ListObjectsOnIsland) { float LengthAvailable = 0; foreach (GameObject Object in ListObjectsOnIsland){ LengthAvailable += AiGet.ObjectWide(Object); } LengthAvailable -= 12; return LengthAvailable ; } // Pour un îlot DOUBLE, ne garde qu'UNE rangée (les caissons arrière) afin de ne pas compter 2x la // longueur ; pour un îlot simple, renvoie la liste telle quelle. static List GetIslandBackRow(List all) { if (all == null || all.Count < 2) return all; Vector3 center = GetIslandCenterPoint(all); Vector3 forward = AiGet.FowardbyObject(all[0]); // axe profondeur var depths = new List(); float minD = float.MaxValue, maxD = float.MinValue; foreach (GameObject o in all) { float v = Vector3.Dot(o.transform.position - center, forward); depths.Add(v); minD = Mathf.Min(minD, v); maxD = Mathf.Max(maxD, v); } if (maxD - minD < 15f) return all; // îlot simple (une seule rangée) // Îlot double : garder la rangée ARRIÈRE (côté +forward). float mid = (minD + maxD) * 0.5f; var row = new List(); for (int i = 0; i < all.Count; i++) if (depths[i] > mid) row.Add(all[i]); return row.Count > 0 ? row : all; } // Dégagement minimum entre une fenêtre et le mur adjacent (le coin de son propre mur) pour qu'elle // puisse porter l'évier. Sous ce seuil, le caisson évier centré dessous déborderait dans le coin, // là où se place le caisson de coin. const float SinkWindowCornerClearance = 30f; static int GetWindowSinkIndex() { //Windows,sinkcabinet for (int i = 0; i < _G.OBJnum; i++) { if (DOIT.exist(i) && _G.OBJs[i][2] == "Window" && WallSelected.Contains( _G.OBJs[i][22].Replace("w","m") )) { float ff = DOIT.ConvertStringToNumber(_G.OBJs[i][21]); if (ff >= 36 && !WindowTooCloseToCorner(i)) { return i; } } } return 1000; } // Vrai si la fenêtre est à moins de SinkWindowCornerClearance d'un bout de son mur : on passe alors // à la fenêtre suivante. Si aucune ne convient, GetWindowSinkIndex renvoie 1000 et AddSinkCabinet // retombe sur son placement sans fenêtre. // Mesuré du BORD de la fenêtre au coin, sur l'emprise nominale (ADN[6] = cadre extérieur) : les // points PBLB sont rentrés de 4" par la moulure et sous-estimeraient la largeur. static bool WindowTooCloseToCorner(int idx) { string wall = _G.OBJs[idx][22]; if (string.IsNullOrEmpty(wall) || wall == "floor" || wall == "ceil") return true; Vector3 start = AiGet.WallStartPoint(wall); Vector3 end = AiGet.WallEndPoint(wall); Vector3 dir = (end - start).normalized; float wallLen = Vector3.Distance(start, end); if (!ObjectExtentOnWall(idx, start, dir, out float x0, out float x1, out _, out _)) return false; float toStart = x0; float toEnd = wallLen - x1; if (toStart >= SinkWindowCornerClearance && toEnd >= SinkWindowCornerClearance) return false; print("[AI] Fenetre " + _G.OBJs[idx][0] + " ecartee pour l'evier : " + toStart + " du debut / " + toEnd + " de la fin de " + wall + " (minimum " + SinkWindowCornerClearance + ")"); return true; } public static Vector3 GetLineIntersection(Vector3 p1, Vector3 dir1, Vector3 p2, Vector3 dir2) { // Flatten to XZ plane Vector2 a = new(p1.x, p1.z); Vector2 b = new(p2.x, p2.z); Vector2 da = new(dir1.x, dir1.z); da = da.normalized; Vector2 db = new(dir2.x, dir2.z); db = db.normalized; Vector2 delta = b - a; float cross = da.x * db.y - da.y * db.x; if (Mathf.Abs(cross) < 1e-6f) { // Lines are parallel return Vector3.zero; } float t = (delta.x * db.y - delta.y * db.x) / cross; Vector2 intersectionXZ = a + da * t; return new Vector3(intersectionXZ.x, 0f, intersectionXZ.y); } public static GameObject GetObjecUnder(GameObject Cabinet) { Vector3 origin = Cabinet.transform.position; Vector3 downDirection = -Cabinet.transform.up; if (Physics.Raycast(origin, downDirection, out RaycastHit hit, 36)) { print("hit.transform.gameObject===" + hit.transform.gameObject); return hit.transform.gameObject; } return null; } public static GameObject GetObjectOver(GameObject Cabinet,float Ofset) { Vector3 origin = Cabinet.transform.position; Vector3 UpDirection = Cabinet.transform.up; Vector3 FrontDirection = -Cabinet.transform.forward; Vector3 RightDirection = Cabinet.transform.right; origin += Ofset*RightDirection; origin.y=0; origin -= 6* FrontDirection ; print("Origine=====" + origin); if (Physics.Raycast(origin, UpDirection, out RaycastHit hit, 36)) { print("hit.transform.gameObject===" + hit.transform.gameObject.name); return hit.transform.gameObject; } return null; } //------------------------------CHECK----------------------------------------------------------------- public bool IsNextWallHaveCornerCab(int IntWall) { //Check if Next wall have a Corner at start bool NextHaveCorner = false; int IntNextWall = IntWall + 1; if (IntNextWall > _G.NW) { IntNextWall = 1; } string WallNextString = "w" + IntNextWall.ToString(); for (int i = 0; i < _G.OBJnum; i++) { if (DOIT.exist(i) && _G.OBJs[i][22] == WallNextString) { //print("_G.OBJs[i][5]====" + _G.OBJs[i][5]); if (_G.OBJs[i][5] == "CB") NextHaveCorner = true; } } return NextHaveCorner; } public static void Swap(List list, int indexA, int indexB) { T temp = list[indexA]; list[indexA] = list[indexB]; list[indexB] = temp; } // Un caisson de la peninsule remplace par un autre modele change de NOM : l'ancien objet est // detruit, un nouveau est cree. Les listes qui le suivent le designent par ce nom — // _peninsulaRowCabs (choix du lave-vaisselle), _peninsulaReturnCabs (rangee d'assise des // tabourets du COTE BOUT), _peninsulaCornerCabs (bornes de l'assise). // Sans cette mise a jour l'entree devient orpheline : le meuble est toujours la, mais il sort // silencieusement du compte. C'est ce qui reduisait les tabourets du COTE BOUT a un seul, le // retour du L etant justement le premier B1 sur "floor" que la passe TIROIRS convertit. // A appeler juste apres la creation du remplacant, avec le nom d'AVANT. static void PeninsulaTrackRename(string oldName) { if (string.IsNullOrEmpty(oldName) || _G.OBJnum <= 0) return; string[] made = _G.OBJs[_G.OBJnum - 1]; if (made == null || string.IsNullOrEmpty(made[0]) || made[0] == "null") return; string newName = made[0]; if (newName == oldName) return; int k = _peninsulaRowCabs.IndexOf(oldName); if (k >= 0) _peninsulaRowCabs[k] = newName; k = _peninsulaCornerCabs.IndexOf(oldName); if (k >= 0) _peninsulaCornerCabs[k] = newName; k = _peninsulaReturnCabs.IndexOf(oldName); if (k >= 0) { _peninsulaReturnCabs[k] = newName; print("[AI] " + oldName + " etait un caisson du RETOUR du L, remplace -> la rangee " + "d'assise le suit sous le nom " + newName + "."); } } public static void ChangeCabinet(GameObject Target, string NewObjectName) { Vector3 PositionToInstall = Target.transform.position; float angle = Target.transform.eulerAngles.y; string WallSelect = GetWallNumberbyObjectName(Target.name); string C40 = GetObjectADN(Target, 40); string C42 = GetObjectADN(Target, 42); // comptoir : a preserver comme les autres string C45 = GetObjectADN(Target, 45); string C47 = GetObjectADN(Target, 47); string C55 = GetObjectADN(Target, 55); string C61 = GetObjectADN(Target, 61); string C63 = GetObjectADN(Target, 63); string C15 = GetObjectADN(Target, 15); string C16 = GetObjectADN(Target, 16); string C17 = GetObjectADN(Target, 17); // Lu AVANT la destruction : GetObjectADN passe par le nom de l'objet. string SinkWide = (DOIT.ConvertStringToNumber(GetObjectADN(Target, 6)) - 4).ToString(); // Libérer l'entrée ADN du caisson remplacé. Sans ça elle reste "vivante" pour DOIT.exist() avec // l'ancienne position et l'ancien modèle : FindClosestBaseCabinet pouvait retenir ce fantôme // comme caisson le plus proche de l'évier, renvoyer un GameObject détruit, et le tiroir suivant // n'était jamais posé. Même traitement que CoroutineAddDishwasher. // Retenu AVANT la destruction : les listes de la peninsule designent l'objet par ce nom. string replacedName = Target.name; int oldIdx = Get.GetObjectIndex(Target.name); Destroy(Target); if (oldIdx >= 0 && _G.OBJs[oldIdx] != null) _G.OBJs[oldIdx][0] = "null"; Set_TemporySC(NewObjectName, WallSelect, PositionToInstall, angle, "F"); // Comptoir : on REPREND celui du caisson remplace, comme les neuf autres champs conserves // ci-dessous. AVANT c'etait "1.25_0_10_0_1.75_0_0_0" en dur, soit 10" de debord ARRIERE // imposes a tout caisson converti — tiroirs, evier ou micro-ondes — alors qu'un caisson de // rangee murale a l'arriere contre le mur et ne doit avoir que la projection ordinaire. // Repli sur cette projection (NoStoolOverhang) si l'ancienne recette est absente ou tronquee. _G.SC[42] = string.IsNullOrEmpty(C42) || C42 == "none" || C42.Split('_').Length < 5 ? DOIT.ChangeCodeOnPos("1.25_0_10_0_1.75_0_0_0", 2, NoStoolOverhang.ToString(), "_") : C42; // SC[40] (montage/éclairage) requis par les caissons muraux : préserver l'existant, défaut sinon // (évite un NullReferenceException dans ConstructWALL sur C[40].Split). _G.SC[40] = string.IsNullOrEmpty(C40) ? "Off_150_F1E6B7?Off_8_F1E6B7?Off_10_F1E6B7?Off_10_F1E6B7" : C40; _G.SC[45] = C45; _G.SC[47] = C47; _G.SC[55] = C55; _G.SC[61] = C61; _G.SC[63] = C63; _G.SC[15] = C15; _G.SC[16] = C16; _G.SC[17] = C17; if (NewObjectName == _AI.BASESINK) { _G.SC[67] = "SINK103,"+SinkWide+",16,#666666_Gloss_None"; _G.SC[68] = "FAUC101,10,10,#666666_Gloss_None"; } CreateCAB.AddCab(); PeninsulaTrackRename(replacedName); } public static bool CheckSpace(Vector3 StartPoint, Vector3 EndPoint, float SpaceNeeded) { //print("Check distance===" + Vector3.Distance(StartPoint, EndPoint)); return SpaceNeeded <= Vector3.Distance(StartPoint, EndPoint); } public static bool CheckCornerAtEnd(string WallNumber, string Category) { //print("CheckCornerAtEn WallNumber======" + WallNumber); float TopLine = _AI.FloorLine + _AI.CabinetHeight; float BaseLine = _AI.FloorLine; if (Category == "Wall") { TopLine = _AI.CeilingLine; BaseLine = _AI.FloorLine + _AI.CabinetHeight+10; } List ListCabinet = new(); if (Category == "Base") { ListCabinet = AiGet.AllObjectsOnWall(WallNumber, "Cabinet", "CornerBASE", TopLine, BaseLine); } if (Category == "Wall") { ListCabinet = AiGet.AllObjectsOnWall(WallNumber, "Cabinet", "CornerWall", TopLine, BaseLine); } if (Category == "Tall") { ListCabinet = AiGet.AllObjectsOnWall(WallNumber, "Cabinet", "T1", TopLine, BaseLine); } NextObject = AiGet.CheckNextWall(WallNumber, TopLine, BaseLine); if (NextObject != null) { ListCabinet.Add(NextObject); } foreach (GameObject Cabinet in ListCabinet) { //print("Cabinet===================================================================================================" + Cabinet.name); Vector3 ObjectPosition = PointAlign(Cabinet.transform.position,_AI.FloorLine); Vector3 EndPoint = AiGet.WallEndPoint(WallNumber); float Distance = Vector3.Distance(ObjectPosition, EndPoint); // print("CheckCornerAtEn Cabinet===" + Cabinet.name+" ObjectPosition ==="+ObjectPosition); // print("CheckCornerAtEn Cabinet===" + Cabinet.name+" EndPoint ==="+EndPoint); // print("CheckCornerAtEn Cabinet===" + Cabinet.name+" distance ==="+Distance); if (Distance < 36) { return true; } } return false; } // public static void CheckON(GameObject Button) // { // print("Name done===="+Button.name); // Button.transform.Find("Toggle").GetComponent().isOn = true; // } public void CloseControlSteps() { Get.o2("Panel_SCENE", "DASH").SetActive(true); Get.o2("Panel_SCENE", "MAIN BTN").SetActive(true); Set.alpha(Get.o2("Panel_SCENE/PNL", "Ai-Controls"), false); Set.alpha(Get.o2("Panel_SCENE", "DASH"), true); Set.alpha(Get.o2("Panel_SCENE", "MAIN BTN"), true); PRICE.Calprice(); } //---------------------Apliances Colors------------------------------------------- // Aligne le MODÈLE d'un appareil DÉJÀ en scène sur la préférence de couleur d'appareils. // Le nom de modèle porte la famille sur ses 4 premiers caractères (STOV/FRID/MICR/DISH) et la // variante de couleur sur les 3 suivants : GetApliancesModel(modèle actuel) renvoie donc le modèle // de la MÊME famille pour la couleur choisie. Retourne true si le modèle a changé (objet reconstruit). // Appelé au début du pipeline par CoroutineAddStove / CoroutineAddFrige, qui ne replacent pas un // appareil existant mais doivent quand même le remettre à la couleur demandée. static bool ApplyAppliancePreference(int idx) { string[] o = _G.OBJs[idx]; if (o == null || o.Length < 5 || string.IsNullOrEmpty(o[1]) || o[1].Length < 7) return false; string wanted = GetApliancesModel(o[1]); if (string.IsNullOrEmpty(wanted) || wanted == o[1]) return false; print("[AI] Appareil " + o[0] + " : modele " + o[1] + " -> " + wanted + " (preference ApliancesColor)"); o[1] = wanted; // Reconstruction explicite : DOIT.REDOOBJECT n'a PAS de branche "frid", il détruirait le frigo // sans le reconstruire. GameObject go = Get.o2("SCENE", o[0]); if (go != null) DestroyImmediate(go); SceneModeManager.SelectedName = o[0]; if (o[4] == "stov") ConstructStove.AddStove(idx); else if (o[4] == "frid") ConstructFRIDGE.AddFridge(idx); else DOIT.REDOOBJECT(idx); return true; } public static string GetApliancesModel(string ModelName) { string colorHex =_G.AI_PREFERENCES["ApliancesColor"]; string ModelCategory=ModelName[..4]; if(ModelCategory=="STOV"){ if (colorHex == "Stainless") {return ModelCategory[..4]+"101";}//Grey if (colorHex == "Black") {return ModelCategory[..4]+"108";}//Black if (colorHex == "White") {return ModelCategory[..4]+"115";}//Withe } if(ModelCategory=="FRID"){ if (colorHex == "Stainless") {return ModelCategory[..4]+"103";}//Grey if (colorHex == "Black") {return ModelCategory[..4]+"113";}//Black if (colorHex == "White") {return ModelCategory[..4]+"121";}//Withe } if(ModelCategory=="MICR"){ if (colorHex == "Stainless") {return ModelCategory[..4]+"101";}//Grey if (colorHex == "Black") {return ModelCategory[..4]+"108";}//Black if (colorHex == "White") {return ModelCategory[..4]+"112";}//Withe } if(ModelCategory=="DISH"){ if (colorHex == "Stainless") {return ModelCategory[..4]+"101";}//Grey if (colorHex == "Black") {return ModelCategory[..4]+"105";}//Black if (colorHex == "White") {return ModelCategory[..4]+"109";}//Withe } return ModelName; } public static string ColorHEX(string colorname) { return colorname.Replace("T_", "") switch// les boutons s'appellent T_Grey, T_Black, T_White, T_Gold, T_Coper { "Stainless" => "333333", "Black" => "2B2A2A", "White" => "ffffff", "Gold" => "EAB972", "Coper" => "F18C5F", _ => "", }; } }