using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; using System; using System.Runtime.InteropServices; // ← ajoute ✅ using System.Text; // ← ajoute ✅ using Unity.Collections; // ← capture AI views ✅ using UnityEngine.Rendering; // ← AsyncGPUReadback ✅ using UnityEngine.Networking; // ← UnityWebRequest ✅ using System.Globalization; // ← nettoyage des accents ✅ // UNITY_STANDALONE_WIN : la version PC (worker de rendu) doit aussi générer le PDF. #if UNITY_EDITOR || UNITY_STANDALONE_WIN using PdfSharpCore; using PdfSharpCore.Pdf; using PdfSharpCore.Drawing; using PdfSharpCore.Drawing.BarCodes; #endif public class PDFCreate : PDFCreation { private static WaitForSeconds _waitForSeconds2 = new WaitForSeconds(2); private List _listItems = new List(); // ① ── WebGL DllImport — DEHORS de tout #if ──────── #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern void GeneratePDFFull( string jsonData, string imagesJson); #endif // ═══════════════════════════════════════════════════════════════════════ // AI — Capture de 2 vues (face + coin) et envoi vers ukitchenit.com/aidesign/images // • Vue 1 : la vue de face (position caméra actuelle) → {email}1.jpg // • Vue 2 : une caméra en coin visant les murs à caissons → {email}2.jpg // email = _G.EMAIL, ou "hb3d63@gmail.com" s'il est vide. // ═══════════════════════════════════════════════════════════════════════ private const int AIVIEW_W = 1280; private const int AIVIEW_H = 750; private const int AIVIEW_JPEG_Q = 85; // À true : améliore les 2 vues via OpenAIImageEnhancer (MyArchitectAI). false = capture brute. public static bool EnhanceViews = false; // Slash final obligatoire : sans lui Apache redirige (301) et le POST devient un GET → corps perdu. private const string AIVIEW_UPLOAD = "https://ukitchenit.com/aidesign/images/"; private const string PDF_UPLOAD = "https://ukitchenit.com/aidesign/pdf/"; /// /// Nettoie un nom pour en faire un nom de fichier sûr, en n'y laissant que les caractères /// acceptés par le PHP de réception (A-Z a-z 0-9 @ . _ -). Les accents sont convertis /// (Côté -> Cote) et les espaces deviennent "_", pour que le fichier local, le fichier sur /// le serveur et resultUrl portent exactement le même nom. /// public static string SafeFileName(string name) { if (string.IsNullOrEmpty(name)) return ""; // FormD décompose "é" en "e" + accent : on retire ensuite les marques. string normalized = name.Trim().Normalize(NormalizationForm.FormD); StringBuilder sb = new StringBuilder(); foreach (char c in normalized) { if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark) continue; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '@' || c == '.' || c == '_' || c == '-') sb.Append(c); else if (c == ' ') sb.Append('_'); } return sb.ToString(); } /// Dossier du PDF local : "F:/PDF CLIENTS" sous Windows, sinon le Bureau. public static string GetLocalPdfFolder() => (Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor) ? @"F:/PDF CLIENTS" : Environment.GetFolderPath(Environment.SpecialFolder.Desktop); /// Chemin complet du PDF généré localement. public static string GetLocalPdfPath() => Path.Combine( GetLocalPdfFolder(), (_G.PDFName != "FN" ? _G.PDFName : "Rapport") + ".pdf"); // Téléverse le PDF généré vers .../render/pdf/{_G.PATH}/{fileName}.pdf private IEnumerator UploadJobPdf(string fileName) { string local = GetLocalPdfPath(); if (!File.Exists(local)) { Debug.LogError("[RenderJob] PDF introuvable pour l'upload : " + local); yield break; } byte[] pdf = File.ReadAllBytes(local); string path = _G.PATH ?? ""; string url = PDF_UPLOAD + "?" + UnityWebRequest.EscapeURL(path); WWWForm form = new WWWForm(); form.AddField("name", fileName); form.AddField("path", path); form.AddField("action", "Upload PDF"); form.AddBinaryData("fileUpload", pdf, fileName + ".pdf", "application/pdf"); using UnityWebRequest req = UnityWebRequest.Post(url, form); req.SetRequestHeader("Origin", "https://ukitchenit.com"); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) Debug.LogError($"[RenderJob] ❌ Upload PDF {fileName} échoué : {req.error}\n{req.downloadHandler.text}"); else Debug.Log($"[RenderJob] ✅ PDF {fileName} envoyé ({pdf.Length / 1024} KB) → {url}"); } private RenderTexture _aiCaptureRT; private byte[] _aiViewBytes; // Vues AI (une par mur avec caissons), conservées pour les pages du PDF (Initialize() ne les efface pas). protected List _aiWallViews = new(); protected List _aiWallLabels = new(); // Recul de la caméra pour la vue d'un mur = largeur du mur * ce facteur. private const float WALL_VIEW_SETBACK = 0.9f; private const float WALL_VIEW_HEIGHT = 72f; // hauteur de la caméra AU-DESSUS DU SOL private const float WALL_VIEW_PITCH = 15f; // inclinaison vers le bas (rotation X, degrés) // Vue d'ensemble : une caméra à chaque coin du rectangle HORS-TOUT de la pièce // ([-WIDE/2..+WIDE/2] x [-DEPTH/2..+DEPTH/2], centré sur l'origine), pointée vers le centre (0,0). private const float OVERVIEW_HEIGHT_Y = 12f; // hauteur ABSOLUE (Y monde), pas au-dessus du sol private const float OVERVIEW_PITCH = 3f; // rotation X (degrés, vers le bas) private const int OVERVIEW_MAX = 4; // plafond d'images (serveur + PDF) private const int AIVIEW_SLOTS = 8; // slots balayés au nettoyage (> OVERVIEW_MAX : rattrape d'anciens plafonds) private const int OVERVIEW_AIM_PASSES = 3; // itérations de recentrage sur les caissons visibles /// Mode AI — posé par LoadXML quand le nom du fichier contient "ai_". protected static bool IsAIMode() => _G.FlieCategory == "ai"; /// Point d'entrée (bouton ou code) : capture une vue par mur avec caissons et les envoie au serveur. public void UploadAIViews() => StartCoroutine(CaptureAndUploadAIViews()); public IEnumerator CaptureAndUploadAIViews() { Camera cam = Camera.main; if (cam == null) { Debug.LogError("[AIViews] Camera.main introuvable"); yield break; } _aiWallViews = new List(); _aiWallLabels = new List(); // Nom des fichiers d'après l'email de l'utilisateur (repli en test). string email = string.IsNullOrEmpty(_G.EMAIL) ? "hb3d63@gmail.com" : _G.EMAIL; print("email =======+email "+email ); bool wasHD = _G.HD; if (!wasHD) DASH.HDsetBTNcolor(true); // Mémoriser la caméra pour la restaurer après. Vector3 pos0 = cam.transform.position; Quaternion rot0 = cam.transform.rotation; // Amélioration IA désactivée (EnhanceViews=false) : capture directe en JPEG, enhancer sauté. OpenAIImageEnhancer enhancer = EnhanceViews ? FindAnyObjectByType() : null; bool usePng = enhancer != null; if (EnhanceViews && enhancer == null) Debug.LogWarning("[AIViews] Aucun OpenAIImageEnhancer actif dans la scène — vues non améliorées."); // VUE D'ENSEMBLE : les 4 coins du rectangle hors-tout de la pièce. Rien n'est masqué — // l'îlot, les lampes et les tabourets font partie de la vue d'ensemble. Vector3[] corners = OverviewCorners(); int idx = 0; for (int c = 0; c < corners.Length && _aiWallViews.Count < OVERVIEW_MAX; c++) { PositionOverviewCamera(cam, corners[c]); yield return new WaitForSeconds(0.4f); // Un coin qui ne cadre aucun caisson (pièce en L, angle vide) ne mérite pas de capture. if (!CameraSeesCabinet(cam)) { Debug.Log($"[AIViews] coin {c + 1} : aucun caisson dans le cadre — capture ignorée."); continue; } byte[] view = null; yield return CaptureAIView(cam, b => view = b, usePng); if (view == null) continue; if (enhancer != null) { Debug.Log($"[AIViews] Amélioration IA de la vue d'ensemble {c + 1}..."); yield return enhancer.EnhanceImage(view, b => view = ToJpeg(b ?? view)); } idx++; _aiWallViews.Add(view); _aiWallLabels.Add("Vue d'ensemble " + idx); yield return UploadAIView(view, email + idx); } // Un rendu précédent a pu laisser plus de vues que celui-ci n'en produit : les slots // au-delà doivent disparaître, sinon l'IA lit des vues périmées à côté des neuves. yield return CleanupExtraAIViews(email, _aiWallViews.Count); // Restaurer la caméra. cam.transform.SetPositionAndRotation(pos0, rot0); if (!wasHD) DASH.HDsetBTNcolor(false); Debug.Log($"[AIViews] {_aiWallViews.Count} vue(s) d'ensemble capturée(s)."); } // Les 4 coins du rectangle hors-tout : la pièce occupe [-WIDE/2..+WIDE/2] x [-DEPTH/2..+DEPTH/2], // centrée sur l'origine (même convention que AI.BuildOccupancyGrid). private static Vector3[] OverviewCorners() { float hw = _G.WIDE * 0.5f; float hd = _G.DEPTH * 0.5f; return new[] { new Vector3(-hw, OVERVIEW_HEIGHT_Y, -hd), new Vector3( hw, OVERVIEW_HEIGHT_Y, -hd), new Vector3( hw, OVERVIEW_HEIGHT_Y, hd), new Vector3(-hw, OVERVIEW_HEIGHT_Y, hd), }; } // Caméra posée sur un coin, cap sur le CENTRE DES CAISSONS VISIBLES. // Le jeu de caissons dans le champ dépend du cap : on part du centre de la pièce (0,0) pour // obtenir un premier champ, puis on recentre et on recommence jusqu'à stabilisation. private void PositionOverviewCamera(Camera cam, Vector3 corner) { Vector3 target = Vector3.zero; AimCamera(cam, corner, target); // Rien de visible depuis ce coin : on garde le cap centre-pièce, la boucle d'appel écartera // la capture de toute façon. if (VisibleCabinetsCenter(cam, out Vector3 first)) { Vector3 lastGood = target; // dernier cap qui cadrait effectivement des caissons target = first; for (int pass = 0; pass < OVERVIEW_AIM_PASSES; pass++) { AimCamera(cam, corner, target); if (!VisibleCabinetsCenter(cam, out Vector3 c)) { AimCamera(cam, corner, lastGood); // le nouveau cap les perd -> revenir au précédent break; } lastGood = target; // Cap stabilisé : le centre du champ ne bouge plus. if (Mathf.Abs(c.x - target.x) < 1f && Mathf.Abs(c.z - target.z) < 1f) break; target = c; } } Debug.Log($"[AIViews] vue d'ensemble pos={cam.transform.position} rot={cam.transform.eulerAngles} cible={target}"); } // Pose la caméra sur le coin et la cape vers 'target' projeté à l'horizontale (fixe la rotation Y), // puis applique le pitch imposé — X et Z remis à plat. private static void AimCamera(Camera cam, Vector3 corner, Vector3 target) { cam.transform.position = corner; Vector3 flat = new Vector3(target.x, corner.y, target.z); if ((flat - corner).sqrMagnitude < 0.0001f) flat = corner + Vector3.forward; // cible confondue cam.transform.LookAt(flat); Vector3 e = cam.transform.eulerAngles; cam.transform.eulerAngles = new Vector3(OVERVIEW_PITCH, e.y, 0f); } // Centre de l'englobant des caissons DANS LE CHAMP de la caméra. false si aucun. // Test d'englobant sans occlusion : sert au cadrage, pas à juger de la visibilité réelle. private static bool VisibleCabinetsCenter(Camera cam, out Vector3 center) { center = Vector3.zero; Plane[] planes = GeometryUtility.CalculateFrustumPlanes(cam); Bounds acc = default; bool any = false; for (int i = 0; i < _G.OBJnum; i++) { if (!DOIT.exist(i)) continue; if (!CabinetBounds(_G.OBJs[i], out Bounds b)) continue; if (!GeometryUtility.TestPlanesAABB(planes, b)) continue; if (!any) { acc = b; any = true; } else acc.Encapsulate(b); } if (!any) return false; center = acc.center; return true; } // Au moins un caisson dans le tronc de cône ? Sert à écarter les coins qui ne cadrent rien. private static bool CameraSeesCabinet(Camera cam) => VisibleCabinetsCenter(cam, out _); // Englobant monde d'un caisson (union de ses Renderer). false si l'objet n'est pas exploitable. private static bool CabinetBounds(string[] o, out Bounds bounds) { bounds = default; if (o == null || o.Length < 3 || o[2] != "Cabinet") return false; GameObject g = Get.o2("SCENE", o[0]); if (g == null || !g.activeInHierarchy) return false; Renderer[] rends = g.GetComponentsInChildren(); if (rends.Length == 0) return false; bounds = rends[0].bounds; for (int k = 1; k < rends.Length; k++) bounds.Encapsulate(rends[k].bounds); return true; } // OBSOLÈTE — ancien cadrage : une vue par mur (remplacé par la vue d'ensemble aux 4 coins). // Conservé pour référence ; plus appelé. private void PositionWallCamera(Camera cam, string wall) { Vector3 start = AiGet.WallStartPoint(wall); Vector3 end = AiGet.WallEndPoint(wall); Vector3 center = (start + end) * 0.5f; float width = Vector3.Distance(start, end); GameObject wgo = Get.o1(wall.Replace("w", "m")); Vector3 forward = wgo != null ? -wgo.transform.forward.normalized : Vector3.forward; // vers la pièce Vector3 camPos = center + forward * (width * WALL_VIEW_SETBACK); camPos.y = -_G.HEIGHT * 0.5f + WALL_VIEW_HEIGHT; // sol + 72 cam.transform.position = camPos; // Regard horizontal vers le mur (fixe la rotation Y), puis inclinaison de WALL_VIEW_PITCH vers le bas. cam.transform.LookAt(new Vector3(center.x, camPos.y, center.z)); Vector3 e = cam.transform.eulerAngles; cam.transform.eulerAngles = new Vector3(WALL_VIEW_PITCH, e.y, 0f); Debug.Log($"[AIViews] mur {wall} caméra pos={cam.transform.position} rot={cam.transform.eulerAngles}"); } // Chiffres du nom de mur ("m3"/"w3" -> "3"). private static string WallNum(string wall) { string d = ""; foreach (char c in wall) if (char.IsDigit(c)) d += c; return d; } // Ré-encode en JPEG l'image renvoyée par l'enhancer (qui sort du PNG) : les vues sont // téléversées en .jpg et insérées dans le PDF, où le PNG serait bien plus lourd. // Renvoie l'entrée telle quelle si le décodage échoue. private static byte[] ToJpeg(byte[] imageBytes) { if (imageBytes == null || imageBytes.Length == 0) return imageBytes; Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, false); byte[] result = imageBytes; if (tex.LoadImage(imageBytes)) result = tex.EncodeToJPG(AIVIEW_JPEG_Q); else Debug.LogWarning("[AIViews] Image IA indécodable — original conservé."); Destroy(tex); return result; } // Rend la caméra dans une RenderTexture (sans l'UI Overlay) puis encode. // png = true quand l'image part vers l'enhancer IA, qui attend du PNG. private IEnumerator CaptureAIView(Camera cam, Action onDone, bool png = false) { _aiViewBytes = null; bool done = false; var rt = new RenderTexture(AIVIEW_W, AIVIEW_H, 24); var savedRT = cam.targetTexture; cam.targetTexture = rt; cam.Render(); cam.targetTexture = savedRT; // Flip vertical (même convention que PDFViewSaver / PDFCreation). _aiCaptureRT = new RenderTexture(AIVIEW_W, AIVIEW_H, 24); Graphics.Blit(rt, _aiCaptureRT, new Vector2(1f, -1f), new Vector2(0f, 1f)); rt.Release(); Destroy(rt); AsyncGPUReadback.Request(_aiCaptureRT, 0, TextureFormat.RGBA32, req => { var format = _aiCaptureRT.graphicsFormat; uint w = (uint)_aiCaptureRT.width; uint h = (uint)_aiCaptureRT.height; _aiCaptureRT.Release(); Destroy(_aiCaptureRT); _aiCaptureRT = null; if (req.hasError) { Debug.LogError("[AIViews] Erreur GPU readback"); done = true; return; } NativeArray raw = req.GetData(); int iw = (int)w, ih = (int)h; var tex = new Texture2D(iw, ih, TextureFormat.RGBA32, false); var processed = tex.GetRawTextureData(); for (int i = 0; i < raw.Length; i += 4) { int pixel = i / 4; int x = pixel % iw, y = pixel / iw; int f = (x + (ih - 1 - y) * iw) * 4; processed[i] = raw[f]; processed[i + 1] = raw[f + 1]; processed[i + 2] = raw[f + 2]; processed[i + 3] = raw[f + 3]; } _aiViewBytes = png ? ImageConversion.EncodeArrayToPNG(processed.ToArray(), format, w, h) : ImageConversion.EncodeArrayToJPG(processed.ToArray(), format, w, h, 0, AIVIEW_JPEG_Q); Destroy(tex); done = true; }); yield return new WaitUntil(() => done); onDone?.Invoke(_aiViewBytes); } // Envoie une image au serveur : POST multipart vers .../aidesign/images?{PATH} private IEnumerator UploadAIView(byte[] jpg, string fileName) { string path = _G.PATH ?? ""; string url = AIVIEW_UPLOAD + "?" + UnityWebRequest.EscapeURL(path); WWWForm form = new WWWForm(); form.AddField("name", fileName); // ex. "hb3d63@gmail.com1" form.AddField("path", path); form.AddField("action", "Upload Image"); form.AddBinaryData("fileUpload", jpg, fileName + ".jpg", "image/jpeg"); using UnityWebRequest req = UnityWebRequest.Post(url, form); req.SetRequestHeader("Origin", "https://ukitchenit.com"); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) Debug.LogError($"[AIViews] ❌ Upload {fileName} échoué : {req.error}\n{req.downloadHandler.text}"); else Debug.Log($"[AIViews] ✅ {fileName} envoyé ({jpg.Length / 1024} KB) → {url}"); } // Demande au serveur d'effacer .../aidesign/images/{PATH}/{email}{n}.jpg pour n > kept. // Une seule requête : le PHP balaie les slots kept+1 .. AIVIEW_SLOTS. private IEnumerator CleanupExtraAIViews(string email, int kept) { string path = _G.PATH ?? ""; string url = AIVIEW_UPLOAD + "?" + UnityWebRequest.EscapeURL(path); WWWForm form = new WWWForm(); form.AddField("name", email); // base du nom, sans le numéro form.AddField("path", path); form.AddField("action", "Delete Extra Images"); form.AddField("keep", kept); form.AddField("slots", AIVIEW_SLOTS); // marge au-dessus d'OVERVIEW_MAX (anciens plafonds) using UnityWebRequest req = UnityWebRequest.Post(url, form); req.SetRequestHeader("Origin", "https://ukitchenit.com"); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) Debug.LogError($"[AIViews] ❌ Nettoyage des vues > {kept} échoué : {req.error}\n{req.downloadHandler.text}"); else Debug.Log($"[AIViews] 🧹 Vues au-delà de {kept} supprimées → {req.downloadHandler.text}"); } // ── Calculate — DEHORS du #if ───────────────────── public override IEnumerator Calculate() { WaitCircle.Setting(false, ""); yield return null; _handler.Close2DPlan(); DASH.HDsetBTNcolor(false); _handler.SetPlan2DButtonOverlayActive(true); // ← Partagé ✅ CollectListItems(); #if UNITY_EDITOR || UNITY_STANDALONE_WIN GenerateLocalPDF(); _M.PH(2, 0, "ffffff", 1, 1); #elif UNITY_WEBGL yield return GenerateWebGLPDF(); #endif // Mode AI : téléverser le PDF, envoyer l'email, puis clore le job de rendu. if (IsAIMode()) { // Le PDF part sur le serveur dans tous les cas (job de rendu ou fichier ouvert à la main). // _G.PDFName (= nom du client, nettoyé) : identique au fichier local et à resultUrl. yield return UploadJobPdf(_G.PDFName); SendClientEmail mailer = FindAnyObjectByType(); if (mailer != null) mailer.SendCurrentDesign(); else Debug.LogWarning("[Email] Aucun SendClientEmail dans la scène — email non envoyé."); // Standalone Windows : signaler au serveur que le job de rendu est terminé. string currentJobId = RenderJobManager.CurrentJobId; RenderJobCompletion renderJobCompletion = FindAnyObjectByType(); // Références prises AVANT Restart.restart(), qui désactive une partie de l'UI. RenderJobManager jobManager = FindAnyObjectByType(); bool wasRenderJob = !string.IsNullOrEmpty(currentJobId); if (wasRenderJob && renderJobCompletion == null) Debug.LogWarning("[RenderJob] Aucun RenderJobCompletion dans la scène — job non clôturé côté serveur."); if (wasRenderJob && renderJobCompletion != null) { string resultUrl = PDF_UPLOAD + _G.PATH + "/" + _G.PDFName + ".pdf"; // yield return direct (sans StartCoroutine) : la coroutine est portée par Calculate, // donc ça marche même si ce GameObject-ci est inactif. yield return renderJobCompletion.CompleteJob( currentJobId, resultUrl ); RenderJobManager.CurrentJobId = ""; // job consommé } // Retour au point de départ, prêt pour le prochain job. yield return _waitForSeconds2; Restart.restart(); // Worker : relancer automatiquement la recherche du job suivant (relance propre). if (wasRenderJob && jobManager != null) { if (jobManager.gameObject.activeInHierarchy) jobManager.RestartJobSearch(); else Debug.LogWarning("[RenderJob] RenderJobManager inactif après Restart — recherche non relancée. " + "Place-le hors de Panel_SCENE (ex. sous HIDER, comme BtnRenderServer)."); } } yield break; } public static IEnumerator CloseMessage() { yield return _waitForSeconds2; Get.o2("HIDER", "MESSAGE").SetActive(false); } // ───────────────────────────────────────────────────── #if UNITY_WEBGL && !UNITY_EDITOR private IEnumerator GenerateWebGLPDF() { yield return new WaitForEndOfFrame(); // ── Encode logos ────────────────────────────── string logo1B64 = _G.LOGO != null ? "data:image/png;base64," + Convert.ToBase64String(_G.LOGO.EncodeToPNG()) : ""; string logo2B64 = _G.LOGO2 != null ? "data:image/png;base64," + Convert.ToBase64String(_G.LOGO2.EncodeToPNG()) : ""; // ── Collecte items liste ────────────────────── //CollectListItemsWebGL(); // ── Construit JSON images ───────────────────── var imagesList = new List(); // Après la vue principale, avant Plan 2D if (_bytes != null) imagesList.Add(BuildImageEntry("Vue principale", Convert.ToBase64String(_bytes))); // image 1 // ← Vues AI par mur (mode "ai_"), après la vue principale. if (IsAIMode() && _aiWallViews != null) for (int wv = 0; wv < _aiWallViews.Count; wv++) imagesList.Add(BuildImageEntry(_aiWallLabels[wv], Convert.ToBase64String(_aiWallViews[wv]))); // ← SavedViews juste après if (BYTESSavedViews != null) for (int i = 0; i < BYTESSavedViews.Length; i++) if (BYTESSavedViews[i] != null) imagesList.Add(BuildImageEntry( _G.SaveCameraView[i].Name, Convert.ToBase64String(BYTESSavedViews[i]))); if (_bytesP2D != null) imagesList.Add(BuildImageEntry("Plan 2D", Convert.ToBase64String(_bytesP2D))); for (int i = 0; i < _G.NW; i++) if (BYTES[i] != null) imagesList.Add(BuildImageEntry( "Élévation " + (i + 1), Convert.ToBase64String(BYTES[i]))); if (_islandBYTES != null) imagesList.Add(BuildImageEntry( "Îlot", Convert.ToBase64String(_islandBYTES))); string imagesJson = "[" + string.Join(",", imagesList) + "]"; // ── Construit JSON données ───────────────────── string listJson = BuildListJSON(); string ci(string key) => _G.ClientInfo.ContainsKey(key) ? _G.ClientInfo[key] : ""; // Couleur hex depuis Color Unity string colorBarHex = ColorToHex(_G.colorBar); string colorTextHex = ColorToHex(_G.colorText); string jsonData = "{" + "\"fileName\":" + JsonStr(_G.PDFName) + "," + "\"colorBar\":" + JsonStr(colorBarHex) + "," + "\"colorText\":" + JsonStr(colorTextHex) + "," + "\"showBarcode\":" + (Get.PreferenceBool("SHOW_BARCODE") ? "true" : "false") + "," + // ← ajoute ✅ "\"isDESIGNER\":" + (_P.ListSwitch["VERSION_DESIGNER"] ? "true" : "false") + "," + // ← ajoute ✅ "\"logo1\":" + JsonStr(logo1B64) + "," + "\"logo2\":" + JsonStr(logo2B64) + "," + "\"projet\":" + JsonStr(ci("Project")) + "," + "\"nom\":" + JsonStr(ci("Name")) + "," + "\"adresse\":" + JsonStr(ci("Address")) + "," + "\"email\":" + JsonStr(ci("Email")) + "," + "\"phone\":" + JsonStr(ci("Phone")) + "," + "\"cuisiniste\":" + JsonStr(ci("Designer")) + "," + "\"storeAddr\":" + JsonStr(ci("StoreAddress")) + "," + "\"note\":" + JsonStr(ci("Note")) + "," + "\"numero\":" + JsonStr(ci("JobNumber")) + "," + "\"priceTotal\":" + JsonStr(_G.PriceTotal) + "," + "\"estimationPhrase\":" + JsonStr(TRANS.This("P_EstimationPhrase")) + "," + "\"listItems\":" + listJson + "}"; GeneratePDFFull(jsonData, imagesJson); _M.PH(2, 0, "ffffff", 1, 1); StaticCoroutine.Start(Save_LOCAL.CloseMessage()); } private string BuildImageEntry(string title, string b64) { return "{\"title\":" + JsonStr(title) + ",\"data\":\"data:image/png;base64," + b64 + "\"}"; } private string BuildListJSON() { var sb = new System.Text.StringBuilder("["); for (int i = 0; i < _listItems.Count; i++) { var it = _listItems[i]; string numero = (it.numero ?? ""); // ← plus de Replace ✅ string qty = (it.qty ?? ""); string code = (it.code ?? ""); string desc = (it.description ?? ""); string pUnit = (it.priceUnit ?? ""); string pTotal = (it.priceTotal ?? ""); sb.Append("{"); sb.Append("\"numero\":" + JsonStr(numero) + ","); sb.Append("\"qty\":" + JsonStr(qty) + ","); sb.Append("\"code\":" + JsonStr(code) + ","); sb.Append("\"description\":" + JsonStr(desc) + ","); sb.Append("\"priceUnit\":" + JsonStr(pUnit) + ","); sb.Append("\"priceTotal\":" + JsonStr(pTotal)); sb.Append("}"); if (i < _listItems.Count - 1) sb.Append(","); } sb.Append("]"); return sb.ToString(); } // ── Helpers ─────────────────────────────────────────── private string JsonStr(string s) { if (s == null) s = ""; s = s.Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\n", "\\n") .Replace("\r", ""); return "\"" + s + "\""; } private string ColorToHex(Color c) { return ((int)(c.r * 255)).ToString("X2") + ((int)(c.g * 255)).ToString("X2") + ((int)(c.b * 255)).ToString("X2"); } #endif #if UNITY_EDITOR || UNITY_STANDALONE_WIN private PdfDocument _pdfDoc; private XFont _fontTitle; private XFont _fontSmall; private XFont _fontLabel; private XFont _fontRow; private XFont _fontFooter; private XFont _fontHeader; private XSolidBrush _headerBgBrush; private XSolidBrush _headerTextBrush; private XSolidBrush _darkBrush; private XSolidBrush _grayBrush; private XSolidBrush _rowBg2; private XPen _borderPen; private XPen _linePen; private double _pageW; private double _pageH; private double _margin = 20; private double _headerH = 30; private double _cartoH = 110; private int _pageNum = 0; private int _totalPages = 0; // ───────────────────────────────────────────────── // GÉNÉRATION PDF // ───────────────────────────────────────────────── private void GenerateLocalPDF() { Directory.CreateDirectory(GetLocalPdfFolder()); // crée le dossier s'il n'existe pas string path = GetLocalPdfPath(); _pageNum = 0; _pdfDoc = new PdfDocument(); _pdfDoc.Info.Title = _G.PDFName; InitStyles(); // ── Compte total pages ──────────────────── _totalPages = 0; if (_bytes != null) _totalPages++; if (_bytesP2D != null) _totalPages++; for (int i = 0; i < _G.NW; i++) if (BYTES[i] != null) _totalPages++; if (_islandBYTES != null) _totalPages++; if (BYTESSavedViews != null) // ← ajouter foreach (var sv in BYTESSavedViews) if (sv != null) _totalPages++; // ← Vues AI par mur (mode "ai_") : une page par mur avec caissons. if (IsAIMode() && _aiWallViews != null) _totalPages += _aiWallViews.Count; if (_listItems.Count > 0) _totalPages += Mathf.CeilToInt(_listItems.Count / 30f) + 1; // ── Pages images ────────────────────────── if (_bytes != null) AddImagePage(_bytes, "Vue principale"); // image 1 // ← Vues AI par mur, juste après la vue principale if (IsAIMode() && _aiWallViews != null) for (int wv = 0; wv < _aiWallViews.Count; wv++) AddImagePage(_aiWallViews[wv], _aiWallLabels[wv]); // ← SavedViews juste après la vue principale if (BYTESSavedViews != null) for (int i = 0; i < BYTESSavedViews.Length; i++) if (BYTESSavedViews[i] != null) { string svLabel = (i < _G.SaveCameraView.Count) ? _G.SaveCameraView[i].Name : $"Vue {i + 1}"; AddImagePage(BYTESSavedViews[i], svLabel); } if (_bytesP2D != null) AddImagePage(_bytesP2D, "Plan 2D"); for (int i = 0; i < _G.NW; i++) if (BYTES[i] != null) AddImagePage(BYTES[i], "Élévation " + (i + 1)); if (_islandBYTES != null) AddImagePage(_islandBYTES, "Îlot"); // Le total du bas lit _G.PriceTotal, qui n'est mis à jour QUE par les éditions de scène // (_MOL.SetMolding, Addto, Resize, undo/redo...). Un design chargé puis exporté directement — ou // généré par l'IA sans option de moulure, auquel cas CoroutineAddMoldings sort avant SetMolding — // le laisse à sa valeur initiale "0", et la ligne de total disparaissait. On le recalcule ici. // Un échec de calcul ne doit pas priver l'utilisateur de tout le PDF : on garde la valeur // précédente et on log. try { PRICE.Calprice(); } catch (Exception e) { Debug.LogError("[PDF] Calcul du prix total en échec : " + e); } if (_listItems.Count > 0) AddListPages(); _pdfDoc.Save(path); // Ouvrir le PDF dans le lecteur par défaut — sauf en mode AI : le worker de rendu // tourne sans surveillance et accumulerait une fenêtre par job. if (!IsAIMode()) { new System.Threading.Thread(() => { System.Diagnostics.Process.Start( new System.Diagnostics.ProcessStartInfo() { FileName = path, UseShellExecute = true }); }).Start(); } Debug.Log($"✅ PDF généré : {path}"); } // ───────────────────────────────────────────────── // PAGE IMAGE // ───────────────────────────────────────────────── private void AddImagePage(byte[] imageBytes, string pageTitle) { _pageNum++; var page = _pdfDoc.AddPage(); page.Size = PageSize.Letter; page.Orientation = PageOrientation.Landscape; _pageW = page.Width.Point; _pageH = page.Height.Point; bool isFirstPage = _pageNum == 1; //double cartoH = isFirstPage ? _cartoH : _cartoH / 2.5; //double cartoY = _pageH - cartoH - _margin; bool isDESIGNER = _P.ListSwitch["VERSION_DESIGNER"]; double cartoH = isDESIGNER ? (isFirstPage ? _cartoH : _cartoH / 2.5) : 35; double cartoY = _pageH - cartoH - _margin; using (var gfx = XGraphics.FromPdfPage(page)) { DrawHeader(gfx, pageTitle); double imgW = _pageW - _margin * 2; double imgH = imgW / (1280.0 / 750.0); // ← ratio fixe ✅ double imgX = _margin; double imgY = _headerH + 8; // ← juste sous la barre ✅ if (imageBytes != null) { using (var ms = new MemoryStream(imageBytes)) { var img = XImage.FromStream(() => ms); gfx.DrawImage(img, imgX, imgY, imgW, imgH); } } // ── Cartouche ou simple ──────────────────────── if (isDESIGNER) { if (isFirstPage) DrawCartouche(gfx, cartoY); else DrawCartoucheShort(gfx, cartoY); } else { DrawCartoucheSimple(gfx, cartoY); } } } // ───────────────────────────────────────────────── // PAGES LISTE // ───────────────────────────────────────────────── private void AddListPages() { _pageNum++; _pageNum++; var page = _pdfDoc.AddPage(); page.Size = PageSize.Letter; page.Orientation = PageOrientation.Landscape; _pageW = page.Width.Point; _pageH = page.Height.Point; bool isDESIGNER = _P.ListSwitch["VERSION_DESIGNER"]; bool showBarcode = Get.PreferenceBool("SHOW_BARCODE"); double cartoHPage = isDESIGNER ? _cartoH / 2.5 : 35; double cartoY = _pageH - cartoHPage - _margin; double maxY = cartoY - 10; double y = _headerH + 10; XGraphics gfx = XGraphics.FromPdfPage(page); DrawHeader(gfx, "Liste"); // ── Cartouche ou simple ──────────────────────── if (isDESIGNER) DrawCartoucheShort(gfx, cartoY); else DrawCartoucheSimple(gfx, cartoY); // ── Colonnes ────────────────────────────── double col0 = _margin, w0 = 60;//# double col1 = col0 + w0;//Qty double w1 = 30; double col2 = col1 + w1;//CODE double w2 = 150; double col3 = col2 + w2;//Description double w3 = _pageW - col3 - _margin - 80 - 70;//Price double col4 = col3 + w3; double w4 = 70; double col5 = col4 + w4; double w5 = _pageW - col5 - _margin; y = DrawListTableHeader(gfx, y, col0,w0, col1,w1, col2,w2, col3,w3, col4,w4, col5,w5); var borderPenList = new XPen(XColor.FromArgb(180, 180, 180), 0.5); var fRow = new XFont("Arial", 8, XFontStyle.Regular); var fRowBold = new XFont("Arial", 8, XFontStyle.Bold); var fSmallCode = new XFont("Arial", 7, XFontStyle.Regular); var darkBrush = new XSolidBrush(XColor.FromArgb(40, 40, 40)); for (int i = 0; i < _listItems.Count; i++) { // ── Hauteur ligne selon barcode ─────── double rowH = 35; if (y + rowH > maxY) { gfx.Dispose(); _pageNum++; page = _pdfDoc.AddPage(); page.Size = PageSize.Letter; page.Orientation = PageOrientation.Landscape; _pageW = page.Width.Point; _pageH = page.Height.Point; cartoY = _pageH - cartoHPage - _margin; maxY = cartoY - 10; gfx = XGraphics.FromPdfPage(page); DrawHeader(gfx, "Liste (suite)"); if (isDESIGNER) DrawCartoucheShort(gfx, cartoY); else DrawCartoucheSimple(gfx, cartoY); y = _headerH + 10; // double col0 = _margin, w0 = 25; // double col1 = col0 + w0, w1 = 30; // double col2 = col1 + w1, w2 = 150; // double col3 = col2 + w2, w3 = _pageW - col3 - _margin - 80 - 70; // double col4 = col3 + w3, w4 = 80; // double col5 = col4 + w4, w5 = _pageW - col5 - _margin; y = DrawListTableHeader(gfx, y, col0, w0, col1, w1, col2, w2, col3, w3, col4, w4, col5, w5); } // Fond alterné if (i % 2 == 0) { var bg = new XSolidBrush(XColor.FromArgb(248, 248, 248)); gfx.DrawRectangle(bg, _margin, y, _pageW - _margin * 2, rowH); } // Bordures DrawListCell(gfx, borderPenList, col0, y, w0, rowH); DrawListCell(gfx, borderPenList, col1, y, w1, rowH); DrawListCell(gfx, borderPenList, col2, y, w2, rowH); DrawListCell(gfx, borderPenList, col3, y, w3, rowH); DrawListCell(gfx, borderPenList, col4, y, w4, rowH); DrawListCell(gfx, borderPenList, col5, y, w5, rowH); double midY = y + rowH / 2 + 3; // ── # ───────────────────────────────── // gfx.DrawString(_listItems[i].numero ?? "", fRowBold, darkBrush, // new XRect(col0, y, w0, rowH), XStringFormats.Center); Debug.Log("Numéro===="+_listItems[i].numero); // Remplace le DrawString simple du numéro ✅ // ── # groupé 4 par ligne ────────────────────── string numStr = _listItems[i].numero ?? ""; string[] nums = numStr.Split('/'); double numLineH = 8; int perLine = 4; // ← 4 par ligne ✅ // Groupe par 4 var numLines = new List(); for (int n = 0; n < nums.Length; n += perLine) { var group = new List(); for (int g = n; g < Mathf.Min(n + perLine, nums.Length); g++) group.Add(nums[g].Trim()); numLines.Add(string.Join("/", group)); } double numTotalH = numLines.Count * numLineH; double numStartY = y + (rowH - numTotalH) / 2 + numLineH; var fNum = new XFont("Arial", 6, XFontStyle.Regular); foreach (var numLine in numLines) { var size = gfx.MeasureString(numLine, fNum); gfx.DrawString(numLine, fNum, darkBrush, new XPoint(col0 + (w0 - size.Width) / 2, numStartY)); numStartY += numLineH; } // ── QTY ─────────────────────────────── gfx.DrawString(_listItems[i].qty ?? "", fRowBold, darkBrush, new XRect(col1, y, w1, rowH), XStringFormats.Center); // ── CODE + Barcode ──────────────────── string codeText = _listItems[i].code ?? ""; string[] codeParts = codeText.Split('\n'); string mainCode = codeParts[0].Trim(); string subCode = codeParts.Length > 1 ? codeParts[1].Trim() : ""; bool barcodeDrawn = false; if (showBarcode && !string.IsNullOrEmpty(mainCode)) { try { string bcValue = mainCode; string[] parts = mainCode.Split( new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) bcValue = parts[parts.Length - 1]; bcValue = bcValue.Replace("*", "").Trim(); if (!string.IsNullOrEmpty(bcValue) && bcValue.Length > 3) { var bc = new Code3of9Standard(bcValue); // ── Taille strictement dans la colonne ─────── double bcW = w2 - 10; // ← largeur max colonne double bcH = rowH * 0.5; // ← hauteur 50% ligne bc.Size = new XSize(bcW, bcH); bc.TextLocation = TextLocation.None; // ── Position — coin haut gauche + centré ────── double bcX = col2+5; // ← centre colonne + (w2 / 2) double bcY = y + 4; // ← haut de ligne + marge gfx.DrawBarCode(bc, XBrushes.Black, new XPoint(bcX, bcY)); // Texte CODE sous le barcode gfx.DrawString(mainCode, fSmallCode, darkBrush, new XRect(col2, y + bcH + 6, w2, 10), XStringFormats.TopCenter); barcodeDrawn = true; } } catch { barcodeDrawn = false; } } // Fallback texte si pas de barcode if (!barcodeDrawn) { gfx.DrawString(mainCode, fRow, darkBrush, new XPoint(col2 + 3, midY - (string.IsNullOrEmpty(subCode) ? 0 : 5))); if (!string.IsNullOrEmpty(subCode)) gfx.DrawString(subCode, fSmallCode, darkBrush, new XPoint(col2 + 3, midY + 6)); } // ── DESCRIPTION ─────────────────────── DrawWrappedText(gfx, _listItems[i].description ?? "", fRow, darkBrush, col3 + 3, y + 8, w3 - 6); // ── PRICE ───────────────────────────── gfx.DrawString(_listItems[i].priceUnit ?? "", fRow, darkBrush, new XRect(col4, y, w4, rowH), XStringFormats.Center); // ── TOTAL ───────────────────────────── gfx.DrawString(_listItems[i].priceTotal ?? "", fRowBold, darkBrush, new XRect(col5, y, w5, rowH), XStringFormats.Center); y += rowH; } // ── Total prix ──────────────────────────── if (_P.ListSwitch["SHOW_PRICE"] && _G.PriceTotal != "0") { // Le bloc (marge + ligne total + phrase d'estimation) doit tenir AU-DESSUS du cartouche. // Sans ce contrôle, une dernière page bien remplie le faisait dessiner par-dessus le // cartouche — ou hors de la zone utile en mode DESIGNER, où le cartouche est plus haut. const double totalBlockH = 25 + 16 + 14; if (y + totalBlockH > maxY) { gfx.Dispose(); _pageNum++; page = _pdfDoc.AddPage(); page.Size = PageSize.Letter; page.Orientation = PageOrientation.Landscape; _pageW = page.Width.Point; _pageH = page.Height.Point; cartoY = _pageH - cartoHPage - _margin; gfx = XGraphics.FromPdfPage(page); DrawHeader(gfx, "Liste (suite)"); if (isDESIGNER) DrawCartoucheShort(gfx, cartoY); else DrawCartoucheSimple(gfx, cartoY); y = _headerH + 10; } y += 25; var fBold = new XFont("Arial", 9, XFontStyle.Bold); gfx.DrawString($"Total price: {_G.PriceTotal}", fBold, darkBrush, new XRect(0, y, _pageW - _margin, 14), XStringFormats.CenterRight); y += 16; var fItalic = new XFont("Arial", 8, XFontStyle.Italic); gfx.DrawString(TRANS.This("P_EstimationPhrase"), fItalic, darkBrush, new XRect(0, y, _pageW - _margin, 14), XStringFormats.CenterRight); } gfx.Dispose(); } // ───────────────────────────────────────────────── // HEADER // ───────────────────────────────────────────────── private void DrawHeader(XGraphics gfx, string pageTitle) { double hdrMargin = 10; // ← marge gauche/droite ✅ gfx.DrawRectangle(_headerBgBrush, 0, 0, _pageW, _headerH); // Titre centré — inchangé gfx.DrawString(pageTitle, _fontTitle, _headerTextBrush, new XRect(0, 0, _pageW, _headerH), XStringFormats.Center); // Numéro page — marge droite 2x ✅ gfx.DrawString($"Page {_pageNum} / {_totalPages}", _fontTitle, _headerTextBrush, new XRect(0, 0, _pageW - hdrMargin * 2, _headerH), // ← *2 ✅ XStringFormats.CenterRight); // Logo 1 — marge gauche 2x ✅ double logo1W = 0; if (_G.LOGO != null) { logo1W = _headerH * 3; byte[] b = _G.LOGO.EncodeToPNG(); using (var ms = new MemoryStream(b)) { var img = XImage.FromStream(() => ms); gfx.DrawImage(img, hdrMargin * 2, 0, logo1W, _headerH); // ← *2 ✅ } } // Logo 2 — après logo 1 ✅ if (_G.LOGO2 != null) { double logo2W = _headerH * 3; byte[] b2 = _G.LOGO2.EncodeToPNG(); using (var ms = new MemoryStream(b2)) { var img = XImage.FromStream(() => ms); gfx.DrawImage(img, hdrMargin * 2 + logo1W, 0, logo2W, _headerH); // ← *2 ✅ } } } // ───────────────────────────────────────────────── // CARTOUCHE COMPLÈTE // ───────────────────────────────────────────────── private void DrawCartoucheSimple(XGraphics gfx, double cartoY) { double cy = cartoY; double fH = 30; // hauteur simple var fLabel = new XFont("Arial", 9, XFontStyle.Regular); var fDate = new XFont("Arial", 9, XFontStyle.Regular); var dark = new XSolidBrush(XColor.FromArgb(40, 40, 40)); // Nom du fichier gfx.DrawString( _G.PDFName != "FN" ? _G.PDFName : TRANS.This("D_FN"), fLabel, dark, new XPoint(_margin, cy + 14)); // Date dessous gfx.DrawString( DateTime.Now.ToString("yyyy-MM-dd"), fDate, dark, new XPoint(_margin, cy + 26)); } private void DrawCartouche(XGraphics gfx, double cartoY) { string projet = _G.ClientInfo.ContainsKey("Project") ? _G.ClientInfo["Project"] : ""; string nom = _G.ClientInfo.ContainsKey("Name") ? _G.ClientInfo["Name"] : ""; string adresse = _G.ClientInfo.ContainsKey("Address") ? _G.ClientInfo["Address"] : ""; string email = _G.ClientInfo.ContainsKey("Email") ? _G.ClientInfo["Email"] : ""; string phone = _G.ClientInfo.ContainsKey("Phone") ? _G.ClientInfo["Phone"] : ""; string cuisiniste = _G.ClientInfo.ContainsKey("Designer") ? _G.ClientInfo["Designer"] : ""; string storeAddr = _G.ClientInfo.ContainsKey("StoreAddress")? _G.ClientInfo["StoreAddress"]: ""; string note = _G.ClientInfo.ContainsKey("Note") ? _G.ClientInfo["Note"] : ""; string numero = _G.ClientInfo.ContainsKey("JobNumber") ? _G.ClientInfo["JobNumber"] : ""; string date = DateTime.Now.ToShortDateString(); double leftW = _pageW * 0.5; double rightW = _pageW - leftW - _margin * 2; double cx = _margin; double cy = cartoY; double rowH = _cartoH / 5.0; double rx = _margin + leftW; double dateW = rightW * 0.5; double numW = rightW * 0.5; var grayLbl = new XSolidBrush(XColor.FromArgb(100, 100, 100)); gfx.DrawRectangle(_borderPen, XBrushes.White, cx, cy, _pageW - _margin * 2, _cartoH); // Ligne 1 : Projet | Date | Numéro DrawCell(gfx, XBrushes.White, cx, cy, leftW, rowH); DrawLabel(gfx, grayLbl, "Projet :", cx + 4, cy, leftW, rowH); DrawValue(gfx, _darkBrush, projet, cx + 4, cy, leftW, rowH); DrawCell(gfx, XBrushes.White, rx, cy, dateW, rowH); DrawLabel(gfx, grayLbl, "Date :", rx + 4, cy, dateW, rowH); DrawValue(gfx, _darkBrush, date, rx + 4, cy, dateW, rowH); DrawCell(gfx, XBrushes.White, rx + dateW, cy, numW, rowH); DrawLabel(gfx, grayLbl, "Numéro :", rx + dateW + 4, cy, numW, rowH); DrawValue(gfx, _darkBrush, numero, rx + dateW + 4, cy, numW, rowH); // Ligne 2 : Nom | Cuisiniste DrawCell(gfx, _rowBg2, cx, cy + rowH, leftW, rowH); DrawLabel(gfx, grayLbl, "Nom :", cx + 4, cy + rowH, leftW, rowH); DrawValue(gfx, _darkBrush, nom, cx + 4, cy + rowH, leftW, rowH); DrawCell(gfx, _rowBg2, rx, cy + rowH, rightW, rowH); DrawLabel(gfx, grayLbl, "Cuisiniste :", rx + 4, cy + rowH, rightW, rowH); DrawValue(gfx, _darkBrush, cuisiniste, rx + 4, cy + rowH, rightW, rowH); // Ligne 3 : Adresse | Store Address DrawCell(gfx, XBrushes.White, cx, cy + rowH * 2, leftW, rowH); DrawLabel(gfx, grayLbl, "Adresse :", cx + 4, cy + rowH * 2, leftW, rowH); DrawValue(gfx, _darkBrush, adresse, cx + 4, cy + rowH * 2, leftW, rowH); DrawCell(gfx, XBrushes.White, rx, cy + rowH * 2, rightW, rowH); DrawLabel(gfx, grayLbl, "Store Address :", rx + 4, cy + rowH * 2, rightW, rowH); DrawValue(gfx, _darkBrush, storeAddr, rx + 4, cy + rowH * 2, rightW, rowH); // Ligne 4 : Email | Note span 2 DrawCell(gfx, _rowBg2, cx, cy + rowH * 3, leftW, rowH); DrawLabel(gfx, grayLbl, "Email :", cx + 4, cy + rowH * 3, leftW, rowH); DrawValue(gfx, _darkBrush, email, cx + 4, cy + rowH * 3, leftW, rowH); DrawCell(gfx, _rowBg2, rx, cy + rowH * 3, rightW, rowH * 2); DrawLabel(gfx, grayLbl, "Note :", rx + 4, cy + rowH * 3, rightW, rowH * 2); DrawValue(gfx, _darkBrush, note, rx + 4, cy + rowH * 3, rightW, rowH * 2); // Ligne 5 : Phone DrawCell(gfx, XBrushes.White, cx, cy + rowH * 4, leftW, rowH); DrawLabel(gfx, grayLbl, "Phone :", cx + 4, cy + rowH * 4, leftW, rowH); DrawValue(gfx, _darkBrush, phone, cx + 4, cy + rowH * 4, leftW, rowH); } // ───────────────────────────────────────────────── // CARTOUCHE COURTE // ───────────────────────────────────────────────── private void DrawCartoucheShort(XGraphics gfx, double cartoY) { string projet = _G.ClientInfo.ContainsKey("Project") ? _G.ClientInfo["Project"] : ""; string nom = _G.ClientInfo.ContainsKey("Name") ? _G.ClientInfo["Name"] : ""; string cuisiniste = _G.ClientInfo.ContainsKey("Designer") ? _G.ClientInfo["Designer"] : ""; string numero = _G.ClientInfo.ContainsKey("JobNumber")? _G.ClientInfo["JobNumber"]: ""; string date = DateTime.Now.ToShortDateString(); double cartoHShort = _cartoH / 2.5; double leftW = _pageW * 0.5; double rightW = _pageW - leftW - _margin * 2; double cx = _margin; double cy = _pageH - cartoHShort - _margin; double rowH = cartoHShort / 2.0; double rx = _margin + leftW; double dateW = rightW * 0.5; double numW = rightW * 0.5; var grayLbl = new XSolidBrush(XColor.FromArgb(100, 100, 100)); gfx.DrawRectangle(_borderPen, XBrushes.White, cx, cy, _pageW - _margin * 2, cartoHShort); // Ligne 1 : Projet | Date | Numéro DrawCell(gfx, XBrushes.White, cx, cy, leftW, rowH); DrawLabel(gfx, grayLbl, "Projet :", cx + 4, cy, leftW, rowH); DrawValue(gfx, _darkBrush, projet, cx + 4, cy, leftW, rowH); DrawCell(gfx, XBrushes.White, rx, cy, dateW, rowH); DrawLabel(gfx, grayLbl, "Date :", rx + 4, cy, dateW, rowH); DrawValue(gfx, _darkBrush, date, rx + 4, cy, dateW, rowH); DrawCell(gfx, XBrushes.White, rx + dateW, cy, numW, rowH); DrawLabel(gfx, grayLbl, "Numéro :", rx + dateW + 4, cy, numW, rowH); DrawValue(gfx, _darkBrush, numero, rx + dateW + 4, cy, numW, rowH); // Ligne 2 : Nom | Designer DrawCell(gfx, _rowBg2, cx, cy + rowH, leftW, rowH); DrawLabel(gfx, grayLbl, "Nom :", cx + 4, cy + rowH, leftW, rowH); DrawValue(gfx, _darkBrush, nom, cx + 4, cy + rowH, leftW, rowH); DrawCell(gfx, _rowBg2, rx, cy + rowH, rightW, rowH); DrawLabel(gfx, grayLbl, "Designer :", rx + 4, cy + rowH, rightW, rowH); DrawValue(gfx, _darkBrush, cuisiniste,rx + 4, cy + rowH, rightW, rowH); } // ───────────────────────────────────────────────── // HELPERS TABLEAU LISTE // ───────────────────────────────────────────────── private double DrawListTableHeader(XGraphics gfx, double y, double c0, double w0, double c1, double w1, double c2, double w2, double c3, double w3, double c4, double w4, double c5, double w5) { double h = 16; var headerBrush = new XSolidBrush(XColor.FromArgb(50, 50, 50)); var borderPen = new XPen(XColor.FromArgb(100, 100, 100), 0.5); var fHeader = new XFont("Arial", 9, XFontStyle.Bold); gfx.DrawRectangle(headerBrush, _margin, y, _pageW - _margin * 2, h); DrawListCell(gfx, borderPen, c0, y, w0, h); DrawListCell(gfx, borderPen, c1, y, w1, h); DrawListCell(gfx, borderPen, c2, y, w2, h); DrawListCell(gfx, borderPen, c3, y, w3, h); DrawListCell(gfx, borderPen, c4, y, w4, h); DrawListCell(gfx, borderPen, c5, y, w5, h); gfx.DrawString("#", fHeader, XBrushes.White, new XRect(c0, y, w0, h), XStringFormats.Center); gfx.DrawString("QTY", fHeader, XBrushes.White, new XRect(c1, y, w1, h), XStringFormats.Center); gfx.DrawString("CODE", fHeader, XBrushes.White, new XPoint(c2 + 3, y + 11)); gfx.DrawString("DESCRIPTION", fHeader, XBrushes.White, new XRect(c3, y, w3, h), XStringFormats.Center); gfx.DrawString("PRICE", fHeader, XBrushes.White, new XRect(c4, y, w4, h), XStringFormats.Center); gfx.DrawString("TOTAL", fHeader, XBrushes.White, new XRect(c5, y, w5, h), XStringFormats.Center); return y + h + 2; } private void DrawListCell(XGraphics gfx, XPen pen, double x, double y, double w, double h) { gfx.DrawRectangle(pen, XBrushes.Transparent, x, y, w, h); } private void DrawWrappedText(XGraphics gfx, string text, XFont font, XBrush brush, double x, double y, double maxW) { if (string.IsNullOrEmpty(text)) return; var lines = new List(); var allLines = text.Split('\n'); foreach (var line in allLines) { if (lines.Count >= 3) break; // ← max 3 lignes ✅ string remaining = line.Trim(); // ── Coupe si trop long ──────────────────── while (remaining.Length > 0 && lines.Count < 3) { // Mesure combien de caractères rentrent string fit = remaining; while (fit.Length > 0) { var size = gfx.MeasureString(fit, font); if (size.Width <= maxW) break; // Coupe au dernier espace int lastSpace = fit.LastIndexOf(' '); fit = lastSpace > 0 ? fit.Substring(0, lastSpace) : fit.Substring(0, fit.Length - 1); } lines.Add(fit); remaining = remaining.Substring(fit.Length).Trim(); } } // ── Dessine max 3 lignes ────────────────────── double lineH = 9; for (int i = 0; i < Math.Min(lines.Count, 3); i++) { gfx.DrawString(lines[i], font, brush, new XPoint(x, y + i * lineH)); } } // ───────────────────────────────────────────────── // HELPERS CELLULES CARTOUCHE // ───────────────────────────────────────────────── private void DrawCell(XGraphics gfx, XBrush bg, double x, double y, double w, double h) => gfx.DrawRectangle(_borderPen, bg, x, y, w, h); private void DrawLabel(XGraphics gfx, XBrush brush, string text, double x, double y, double w, double h) { var f = new XFont("Arial", 7, XFontStyle.Regular); gfx.DrawString(text, f, brush, new XPoint(x, y + 9)); } private void DrawValue(XGraphics gfx, XBrush brush, string text, double x, double y, double w, double h) { if (string.IsNullOrEmpty(text)) return; gfx.DrawString(text, _fontLabel, brush, new XPoint(x + 30, y + (h / 2) + 3)); } // ───────────────────────────────────────────────── // INIT STYLES // ───────────────────────────────────────────────── private void InitStyles() { _fontTitle = new XFont("Arial", 11, XFontStyle.Bold); _fontSmall = new XFont("Arial", 8, XFontStyle.Regular); _fontLabel = new XFont("Arial", 9, XFontStyle.Regular); _fontRow = new XFont("Arial", 9, XFontStyle.Regular); _fontFooter = new XFont("Arial", 8, XFontStyle.Regular); _fontHeader = new XFont("Arial", 9, XFontStyle.Bold); _headerBgBrush = new XSolidBrush(ColorToXColor(_G.colorBar)); _headerTextBrush = new XSolidBrush(ColorToXColor(_G.colorText)); _darkBrush = new XSolidBrush(XColor.FromArgb(40, 40, 40)); _grayBrush = new XSolidBrush(XColor.FromArgb(150, 150, 150)); _rowBg2 = new XSolidBrush(XColor.FromArgb(235, 235, 235)); _borderPen = new XPen(XColor.FromArgb(100, 100, 100), 0.75); _linePen = new XPen(XColor.FromArgb(210, 210, 210), 0.5); } private XColor ColorToXColor(Color c) => XColor.FromArgb( (int)(c.a * 255), (int)(c.r * 255), (int)(c.g * 255), (int)(c.b * 255)); #endif // ───────────────────────────────────────────────── // COLLECTE DES ITEMS // ───────────────────────────────────────────────── // Armoires normales — renomme l'existant private void CollectListItems() { _listItems.Clear(); List Cablist = DOLIST.CABLISTint(); // ── Armoires ou Box ─────────────────────────── if (!Get.PreferenceBool("CABINET_DOOR_RECIPE")) AddCabItems(Cablist); else AddBoxItems(Cablist); // ── Portes ──────────────────────────────────── if (Get.PreferenceBool("CABINET_DOOR_RECIPE")) AddDoorItems(); // ── Tiroirs ─────────────────────────────────── if (Get.PreferenceBool("CABINET_DRAWER_RECIPE")) AddDrawerItems(); // ── Panneaux ────────────────────────────────── AddPnlItems(); // ── Moulures ────────────────────────────────── AddMoldingItems(Cablist, "FILLER"); if (_P.ListSwitch["VALANCE_SELECT"]) AddMoldingItems(Cablist, "VALANCE"); if (_P.ListSwitch["FASCIA_SELECT"]) AddMoldingItems(Cablist, "FASCIA"); if (_P.ListSwitch["OGEE_SELECT"]) AddMoldingItems(Cablist, "OGEE"); AddMoldingItems(Cablist, "KICK"); // ── Items additionnels ──────────────────────── if (_P.ListSwitch["ADD_ITEM_LIST"]) { foreach (var pair in _G.LibraryItems) { string desc = UIT.Value(Library.Items, pair.Key, "L" + _G.L); _listItems.Add(new PDFItem("", pair.Value.ToString(), pair.Key, desc, "", "")); } } } private void AddCabItems(List Cablist) { if (!Get.PreferenceBool("CABINET_DOOR_RECIPE")) { var CABdict = MakeList.CABLISTQTY(Cablist); foreach (var pair in CABdict) { string PriceColumn = Get.PreferenceBool("STORE_SELECTION") ? _G.StoreZone : ""; string[] CODE = pair.Key.Split('?'); string[] VALUE = pair.Value.Split('?'); string qty = VALUE[0]; string number = VALUE[1]; string DoorLanguage=""; string DoorUniqueName=""; ///Kwizine No Door----------------- string DoorColorUniqueName = TRANS.This("C_MELAMINEWHITE"); if(CODE[3]!="MELAMINEWHITE" && CODE[2]!="NO_DOOR"){ DoorColorUniqueName = UIT.Value(Library.CabTexture,CODE[3],Header.Unique_Name); } if(CODE[2]!="NO_DOOR"){ DoorLanguage = " "+UIT.Value(Library.Door,CODE[2],"L"+_G.L.ToString()); DoorColorUniqueName=" "+UIT.Value(Library.CabTexture,CODE[3],Header.Unique_Name); DoorUniqueName =" "+UIT.Value(Library.Door,CODE[2],Header.Unique_Name); } if(CODE[2]=="NO_DOOR" && CODE[3]=="MELAMINEWHITE"){ DoorUniqueName=" "+TRANS.This("L_No doors"); DoorColorUniqueName=" "+TRANS.This("L_No doors"); } if(CODE[2]=="NO_DOOR" && CODE[3]!="MELAMINEWHITE" && CODE[3].IndexOf("TEXT")==-1){ DoorUniqueName=" "+TRANS.This("L_No doors"); DoorColorUniqueName=" "+UIT.Value(Library.CabTexture,CODE[3],Header.Unique_Name); } if(CODE[2]=="NO_DOOR" && CODE[3].IndexOf("TEXT")!=-1){ DoorUniqueName=" "+TRANS.This("L_No doors"); DoorColorUniqueName=" "+TRANS.This("L_No doors"); } ///Kwizine No Door----------------- // ── Code ────────────────────────────────── string PriceCode_ID = UIT.GetCabinetPriceID(CODE[0], CODE[2], CODE[3]); string CodeFromECommerce = ""; if (Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE")) CodeFromECommerce = UIT.Value(Library.Price, PriceCode_ID, Header.Ecommerce_ID); string CUP = ""; if (Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE")) CUP = UIT.Value(Library.Price, PriceCode_ID, Header.CUP); string code = Get.PreferenceBool("SHOW_BARCODE") ? PriceCode_ID + " " + CUP : PriceCode_ID + "\n" + CodeFromECommerce; // ── Description ─────────────────────────── //string DoorLanguage = ""; //string DoorColorUniqueName = TRANS.This("C_MELAMINEWHITE"); //string DoorUniqueName = ""; string Door_Recipe = " " + UIT.Value(Library.Cabinet, CODE[0], Header.Door_Recipe); if (UIT.Ind(Library.Cabinet, Header.Door_Recipe) == 0 || CODE[2] == "NO_DOOR") Door_Recipe = ""; if (CODE[3] != "MELAMINEWHITE" && CODE[2] != "NO_DOOR") DoorColorUniqueName = UIT.Value(Library.CabTexture, CODE[3], Header.Unique_Name); if (CODE[2] != "NO_DOOR") { DoorLanguage = " " + UIT.Value(Library.Door, CODE[2], "L" + _G.L); DoorColorUniqueName= " " + UIT.Value(Library.CabTexture, CODE[3], Header.Unique_Name); DoorUniqueName = " " + UIT.Value(Library.Door, CODE[2], Header.Unique_Name); } if (CODE[2] == "NO_DOOR" && CODE[3] == "MELAMINEWHITE") { DoorUniqueName = " " + TRANS.This("L_No doors"); DoorColorUniqueName= " " + TRANS.This("L_No doors"); } string Size = CODE[1]; string CabLanguage= CODE[4]; string des = TRANS.This("G_CABINET") + ": "; des += CabLanguage + DoorLanguage + " / " + DoorColorUniqueName; int DesLength = des.Length + Door_Recipe.Length + 3; des += DesLength < 138 ? " " + Door_Recipe : "\n" + Door_Recipe; DesLength = des.Length + Size.Length + 3; des += DesLength > 138 ? " " + Size : "\n" + Size; if (des.Length > 207) des = des[..207]; // ── Prix ────────────────────────────────── string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, PriceCode_ID, Header.Price + PriceColumn)); float qty_f = DOIT.ConvertStringToNumber(VALUE[0]); priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * qty_f * 100f) / 100f).ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem( number, qty, code, des, priceUnit, priceTotal)); //objCount++; } } } // ── Box (sans portes) — copie de AddFieldForBox ── private void AddBoxItems(List Cablist) { string PriceColumn = Get.PreferenceBool("STORE_SELECTION") ? _G.StoreZone : ""; var CABdict = MakeList.CABLISTQTY(Cablist); foreach (var pair in CABdict) { string[] CODE = pair.Key.Split('?'); string[] VALUE = pair.Value.Split('?'); string qty = VALUE[0]; string number = VALUE[1]; string PriceCode_ID = UIT.GetCabinetPriceID(CODE[0], "NO_DOOR", "MELAMINEWHITE"); string CodeFromECommerce = Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE") ? UIT.Value(Library.Price, PriceCode_ID, Header.Ecommerce_ID) : ""; string CUP = Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE") ? UIT.Value(Library.Price, PriceCode_ID, Header.CUP) : ""; string code = Get.PreferenceBool("SHOW_BARCODE") ? PriceCode_ID + " " + CUP : PriceCode_ID + "\n" + CodeFromECommerce; string des = TRANS.This("G_CABINET") + ": " + CODE[4] + " " + TRANS.This("L_No doors") + "\n" + CODE[1]; string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, PriceCode_ID, Header.Price + PriceColumn)); float qty_f = DOIT.ConvertStringToNumber(VALUE[0]); priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * qty_f * 100f) / 100f).ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem(number, qty, code, des, priceUnit, priceTotal)); } } // ── Portes — copie de AddFieldForDoor ───────────── private void AddDoorItems() { string PriceColumn = Get.PreferenceBool("STORE_SELECTION") ? _G.StoreZone : ""; var DOORdict = MakeList.DOORLISTQTY(DOLIST.CABLISTint()); foreach (var pair in DOORdict) { string PriceID = pair.Key; string CodeFromECommerce = Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE") ? " / " + UIT.Value(Library.Price, PriceID, Header.Ecommerce_ID) : ""; string CUP = Get.PreferenceBool("SHOW_BARCODE") ? UIT.Value(Library.Price, PriceID, Header.CUP) : ""; string code = Get.PreferenceBool("SHOW_BARCODE") ? PriceID + " " + CUP : PriceID + CodeFromECommerce; string des = UIT.Value(Library.Price, PriceID, "L" + _G.L); string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, PriceID, Header.Price + PriceColumn)); priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * pair.Value * 100f) / 100f).ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem("", pair.Value.ToString(), code, des, priceUnit, priceTotal)); } } // ── Tiroirs — copie de AddFieldForDrawer ────────── private void AddDrawerItems() { string PriceColumn = Get.PreferenceBool("STORE_SELECTION") ? _G.StoreZone : ""; var DRAWERdict = MakeList.DRAWERLISTQTY(DOLIST.CABLISTint()); foreach (var pair in DRAWERdict) { string PriceID = pair.Key; string CodeFromECommerce = Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE") ? " / " + UIT.Value(Library.Price, PriceID, Header.Ecommerce_ID) : ""; string CUP = Get.PreferenceBool("SHOW_BARCODE") ? UIT.Value(Library.Price, PriceID, Header.CUP) : ""; string code = Get.PreferenceBool("SHOW_BARCODE") ? PriceID + " " + CUP : PriceID + CodeFromECommerce; string des = UIT.Value(Library.Price, PriceID, "L" + _G.L); string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, PriceID, Header.Price + PriceColumn)); priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * pair.Value * 100f) / 100f).ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem("", pair.Value.ToString(), code, des, priceUnit, priceTotal)); } } // ── Panneaux — renomme l'existant ───────────────── private void AddPnlItems() { // Panneaux var PNLdict = MakeList.PNLLISTQTY(); foreach (var pair in PNLdict) { string[] CODE = pair.Key.Split('?'); string PanelUniqueName = UIT.Value(Library.Panel, CODE[0], Header.Unique_Name); string ColorUniqueName = UIT.Value(Library.CabTexture, CODE[1], Header.Unique_Name); string Codename = PanelUniqueName + "-" + ColorUniqueName; if (!_G.LibraryItems.ContainsKey(Codename)) { string model = UIT.Value(Library.Panel, CODE[0], Header.Model); string PriceCode_ID = UIT.GetPanelPriceCode_ID(PanelUniqueName, ColorUniqueName, ""); string Code_ID = _P.ListSwitch["PANEL_BY_MODEL_COLOR"] ? PriceCode_ID : PanelUniqueName; if (model.Length >= 2 && model[..2] == "D_") Code_ID = UIT.GetPanelPriceCode_ID(PanelUniqueName, "", "SHA-" + ColorUniqueName); string Wide = UIT.Value(Library.Panel, CODE[0], Header.Wide); if (Wide.Split("_").Length > 2) Wide = Wide.Split("_")[2]; string Height = UIT.Value(Library.Panel, CODE[0], Header.Height); string Thickness = UIT.Value(Library.Panel, CODE[0], Header.Thick); string Size = " " + Wide + " x " + Height + " x " + Thickness; string Category = model.Length >= 2 && model[..2] == "D_" ? "M_DOOR" : "M_PANEL"; // ── Code ECommerce ──────────────────────── string CodeFromECommerce = ""; if (Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE")) CodeFromECommerce = UIT.Value(Library.Price, PriceCode_ID, Header.Ecommerce_ID); string CUP = Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE") ? UIT.Value(Library.Price, PriceCode_ID, Header.CUP) : ""; string code = Get.PreferenceBool("SHOW_BARCODE") ? PriceCode_ID + " " + CUP : PriceCode_ID + "\n" + CodeFromECommerce; // ✅ // ── Description ─────────────────────────── string PanelLanguage = " " + UIT.Value(Library.Panel, CODE[0], "L" + _G.L); string ColorLanguage = " " + UIT.Value(Library.CabTexture, CODE[1], "L" + _G.L); string des = TRANS.This(Category) + ": "; if (des.Length < 120) Size = "\n" + Size; else Size = " " + Size; des += PanelLanguage + ColorLanguage + Size; // ── Prix ────────────────────────────────── string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, PriceCode_ID, Header.Price + _G.StoreZone)); float qty_f = pair.Value; priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * qty_f * 100f) / 100f) .ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem( "", // ← vide comme serveur ✅ pair.Value.ToString(), code, des, priceUnit, priceTotal)); //objCount++; } } } private void AddMoldingItems(List Cablist,string Category){ var dict = new Dictionary(); switch (Category) { case "FILLER": dict = MakeList.FILLERQTY(Cablist); break; case "VALANCE": dict = MakeList.VALANCELISTQTY(); break; case "FASCIA": dict = MakeList.FASCIAQTY(); break; case "OGEE": dict = MakeList.OGEELISTQTY(); break; case "KICK": dict = MakeList.KICKLISTQTY(Cablist); break; } foreach (var pair in dict) { string PriceColumn = Get.PreferenceBool("STORE_SELECTION") ? _G.StoreZone : ""; string[] Code = pair.Key.Split('?'); string CodeUniqueName = UIT.Value(Library.Molding, Code[0], Header.Unique_Name); string ColorUniqueName = UIT.Value(Library.CabTexture, Code[1], Header.Unique_Name); string Height = UIT.Value(Library.Molding, Code[0], Header.Height); string Wide = UIT.Value(Library.Molding, Code[0], Header.Wide); string Size = Height + _G.Sys + " x " + Wide + _G.Sys; float MoldingLength = DOIT.ConvertStringToNumber( Category == "FILLER" ? Height : Wide); float Qty = _P.ListSwitch["FILLER_BY_ITEMS"] && Category == "FILLER" ? pair.Value : Mathf.Ceil(pair.Value / MoldingLength); string Code_ID = _P.ListSwitch["MOLDING_BY_MODEL_COLOR"] ? UIT.GetMoldingPriceCode_ID(CodeUniqueName, ColorUniqueName) : CodeUniqueName; // ── ECommerce ───────────────────────────── string CodeFromECommerce = ""; if (Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE")) CodeFromECommerce = UIT.Value(Header.Price, Code_ID, Header.Ecommerce_ID); string CUP = ""; if (Get.PreferenceBool("PDF_SHOW_ECOMMERCE_CODE")) CUP = UIT.Value(Library.Price, Code_ID, Header.CUP); string code = Get.PreferenceBool("SHOW_BARCODE") ? Code_ID + " " + CUP : Code_ID + "\n" + CodeFromECommerce; // ← ECommerce ✅ // ── Description ─────────────────────────── string des = TRANS.This("M_" + Category) + ": " + " " + UIT.Value(Library.Molding, Code_ID, "L" + _G.L) + "\n" + Size + "\n" + ColorUniqueName; // ── Prix ────────────────────────────────── string priceUnit = ""; string priceTotal = ""; if (_P.ListSwitch["SHOW_PRICE"]) { float uPrice = DOIT.ConvertStringToNumber( UIT.Value(Header.Price, Code_ID, Header.Price + PriceColumn)); priceUnit = uPrice > 0 ? uPrice + "$" : ""; priceTotal = uPrice > 0 ? (Mathf.Round(uPrice * Qty * 100f) / 100f).ToString("F2") + "$" : ""; } _listItems.Add(new PDFItem( "", Qty.ToString(), code, // ← code avec ECommerce ✅ des, priceUnit, priceTotal)); //objCount++; } } }