/** * APEX 4.0 — Blum LEGRABOX pure * Données d'intégration : débit panneaux 16 mm, perçage CNC caisson (coulisses), * perçage façades (INSERTA/EXPANDO), norme suisse SMS, génération de commande (BOM) * et accessoires intérieurs. * * Source : dossier « INTÉGRATION DATA - APEX 4.0 » (modules usinage CNC / SMS / BOM / * accessoires) + catalogue technique Blum. Toutes les cotes en mm. */ /** Épaisseur de référence stricte des fonds et parois arrière de tiroir. */ export const LEGRABOX_PANEL_THICKNESS_MM = 16; /** * Systèmes de hauteur Legrabox (N/M/K/C/F). * - sideHeightMm : hauteur de côté (profil métallique) — « hauteur_cote_mm » * - nicheHeightMm : hauteur de niche / encombrement façade min — « hauteur_des_niches_mm » * - backHeightMm : hauteur C de la paroi arrière 16 mm — « hauteur_C_mm_selon_systeme » * - hch : code hauteur commercial (S/M/L/XL/XXL) * - smsFromSixths : norme suisse SMS — disponible à partir de n/6 (1/6 = 127 mm) */ export const LEGRABOX_SYSTEMS = Object.freeze({ N: Object.freeze({ key: 'N', hch: 'S', sideHeightMm: 66, nicheHeightMm: 80, backHeightMm: 39, smsFromSixths: 1 }), M: Object.freeze({ key: 'M', hch: 'M', sideHeightMm: 90, nicheHeightMm: 106, backHeightMm: 63, smsFromSixths: 1 }), K: Object.freeze({ key: 'K', hch: 'L', sideHeightMm: 128, nicheHeightMm: 144, backHeightMm: 101, smsFromSixths: 2 }), C: Object.freeze({ key: 'C', hch: 'XL', sideHeightMm: 177, nicheHeightMm: 193, backHeightMm: 148, smsFromSixths: 2 }), F: Object.freeze({ key: 'F', hch: 'XXL', sideHeightMm: 240, nicheHeightMm: 257, backHeightMm: 212, smsFromSixths: 3 }) }); /** Hauteurs de côté sélectionnables (remplace l'ancien 60 mm par 66 mm — système N). */ export const LEGRABOX_SIDE_HEIGHTS_MM = Object.freeze([66, 90, 128, 177, 240]); /** Norme suisse SMS — division en sixièmes. */ export const SMS = Object.freeze({ sixthMm: 127, description: 'Système de mesure suisse (Schweizer Mass-System) — division en sixièmes (1/6 = 127 mm)' }); /** @returns {object|null} système Legrabox pour une hauteur de côté (66/90/128/177/240). */ export function getLegraboxSystemForSideHeight(sideHeightMm) { const h = Number(sideHeightMm); for (const sys of Object.values(LEGRABOX_SYSTEMS)) { if (sys.sideHeightMm === h) return sys; } return null; } /** * Hauteur de niche (encombrement façade min) pour une hauteur de côté. * Hauteur inconnue → valeur inchangée (compat anciennes configs). */ export function legraboxNicheHeightMm(sideHeightMm) { const sys = getLegraboxSystemForSideHeight(sideHeightMm); return sys ? sys.nicheHeightMm : Math.max(0, Number(sideHeightMm) || 0); } // ── 1. Débit panneaux — épaisseur 16 mm ───────────────────────────────── /** * Fond de tiroir 16 mm. * longueur A = NL − 10 · largeur B = X − 35 (X = largeur libre du corps). * Usinage spécial : feuillure/renfort 38 × 8 mm à gauche et à droite. * @param {number} nominalLengthMm — longueur nominale NL de la coulisse * @param {number} internalWidthMm — largeur libre du corps (X) */ export function legraboxBottomPanelMm(nominalLengthMm, internalWidthMm) { const nl = Math.max(0, Number(nominalLengthMm) || 0); const x = Math.max(0, Number(internalWidthMm) || 0); if (nl <= 10 || x <= 35) return null; return { lengthMm: nl - 10, widthMm: x - 35, thicknessMm: LEGRABOX_PANEL_THICKNESS_MM, rebate: { widthMm: 38, depthMm: 8, sides: ['left', 'right'], note: 'Fond feuilluré/renforcé à gauche et à droite : 38 × 8 mm' } }; } /** * Paroi arrière (dos de tiroir) 16 mm. * largeur B = X − 38 · hauteur C selon système (N 39 / M 63 / K 101 / C 148 / F 212). * @param {number} internalWidthMm — largeur libre du corps (X) * @param {string} systemKey — 'N' | 'M' | 'K' | 'C' | 'F' */ export function legraboxBackPanelMm(internalWidthMm, systemKey) { const x = Math.max(0, Number(internalWidthMm) || 0); const sys = LEGRABOX_SYSTEMS[systemKey]; if (!sys || x <= 38) return null; return { widthMm: x - 38, heightMm: sys.backHeightMm, thicknessMm: LEGRABOX_PANEL_THICKNESS_MM }; } // ── 2. Perçage CNC — coulisses sur caisson (Système 32) ──────────────── /** * Positions de perçage cumulées (axe X) en mm depuis le bord AVANT du montant, * pour le vissage des coulisses, selon capacité de charge. * Perçage pilote Système 32 : Ø5 × 13 mm (vis à tête plate recommandées par Blum). */ export const LEGRABOX_RAIL_DRILLING = Object.freeze({ compatibleSideHeightsMm: LEGRABOX_SIDE_HEIGHTS_MM, holeDiameterMm: 5, holeDepthMm: 13, /** * Axe vertical de la ligne de vissage du profil de corps, au-dessus du bas * de la niche tiroir. Hypothèse atelier (catalogue Blum) — à confirmer au gabarit ZML.3710. */ screwLineAboveNicheBottomMm: 9.5, byLoadKg: Object.freeze({ 40: Object.freeze([37, 69, 197, 229, 261, 293, 357]), 70: Object.freeze([37, 69, 261, 293, 357]) }) }); /** * Positions X (depuis le chant avant du panneau vertical) pour une capacité donnée, * bornées à la profondeur utile du panneau. * @param {40|70} loadKg * @param {number} [maxXMm] — profondeur maximum perçable (ex. profondeur panneau − retrait) */ export function getLegraboxRailHolePositionsMm(loadKg = 40, maxXMm = Infinity) { const base = LEGRABOX_RAIL_DRILLING.byLoadKg[Number(loadKg) === 70 ? 70 : 40]; const max = Number.isFinite(Number(maxXMm)) ? Number(maxXMm) : Infinity; return base.filter((x) => x <= max); } // ── 3. Perçage CNC — façades (INSERTA / cheville expansible) ──────────── /** * Matrice de perçage façade — fixation INSERTA (cheville expansible Ø10, prof. > 12 mm). * Cotes catalogue conservées telles quelles (validation programmeur CNC) : * - minFrontHeightMm (A) : encombrement min de la façade * - coteC / coteD : positions verticales des 2 perçages du support, depuis le bas façade * - coteB : rangées supplémentaires (façades hautes C / F), depuis le bas façade * - coteE : retrait horizontal de l'axe depuis la référence côté tiroir * - coteG : cote catalogue complémentaire (référence gabarit) */ export const LEGRABOX_FACADE_INSERTA = Object.freeze({ toolNote: 'Cheville expansible Ø 10 mm, profondeur de perçage > 12 mm', holeDiameterMm: 10, holeDepthMm: 13, bySystem: Object.freeze({ N: Object.freeze({ minFrontHeightMm: 80, coteC: 45.5, coteD: 16, coteE: 23.4, coteG: 14 }), M: Object.freeze({ minFrontHeightMm: 106, coteC: 51, coteD: 32, coteE: 23.4, coteG: 14 }), K: Object.freeze({ minFrontHeightMm: 144, coteC: 51, coteD: 32, coteE: 23.4, coteG: 14 }), C: Object.freeze({ minFrontHeightMm: 193, coteB: Object.freeze([147, 179]), coteC: 51, coteD: 32, coteE: 24.4, coteG: 14 }), F: Object.freeze({ minFrontHeightMm: 257, coteB: Object.freeze([179, 211, 243]), coteC: 51, coteD: 32, coteE: 24.4, coteG: 14 }) }) }); /** * Positions XY des perçages INSERTA sur une façade (repère : origine bas-gauche façade). * Interprétation atelier : 2 perçages verticaux par côté (y = coteD bas, y = coteC haut, * depuis le bas façade) + rangées coteB pour C/F. Axe horizontal à coteE de la * référence côté de tiroir (bord de la largeur libre X), soit * x = (largeur façade − X)/2 + coteE depuis chaque bord de façade. * @param {string} systemKey — 'N'…'F' * @param {number} facadeWidthMm * @param {number} facadeHeightMm * @param {number} internalWidthMm — largeur libre du corps X * @returns {{ xMm:number, yMm:number, role:string }[]} */ export function buildLegraboxFacadeInsertaHolePositionsMm(systemKey, facadeWidthMm, facadeHeightMm, internalWidthMm) { const spec = LEGRABOX_FACADE_INSERTA.bySystem[systemKey]; const W = Math.max(0, Number(facadeWidthMm) || 0); const H = Math.max(0, Number(facadeHeightMm) || 0); const X = Math.max(0, Number(internalWidthMm) || 0); if (!spec || W <= 0 || H <= 0 || X <= 0 || W <= X - 2 * spec.coteE) return []; const inset = Math.max(spec.coteG, (W - X) / 2 + spec.coteE); const xs = [ { xMm: Math.round(inset * 10) / 10, side: 'gauche' }, { xMm: Math.round((W - inset) * 10) / 10, side: 'droite' } ]; const ys = [ { yMm: spec.coteD, role: 'bas' }, { yMm: spec.coteC, role: 'haut' }, ...(Array.isArray(spec.coteB) ? spec.coteB.map((b, i) => ({ yMm: b, role: `rangee_B${i + 1}` })) : []) ].filter((p) => p.yMm > 0 && p.yMm < H); const out = []; for (const { xMm, side } of xs) { for (const { yMm, role } of ys) { out.push({ xMm, yMm, role: `${side}_${role}` }); } } return out; } // ── 4. Montage / ajustement ────────────────────────────────────────────── export const LEGRABOX_ASSEMBLY = Object.freeze({ montageFacade: 'Sans outils', demontageFacade: 'Tournevis', reglageHauteur: "Depuis l'intérieur (devant)", reglageInclinaison: 'Depuis le bas (devant)', technologiesOuvertureFermeture: Object.freeze([ 'Blumotion (Fermeture automatique avec amortissement)', 'Tip-on Blumotion (Ouverture automatique avec amortissement intégré)', 'Tip-on (Système d\u2019ouverture mécanique)' ]) }); // ── 5. Génération de commande (BOM) ───────────────────────────────────── export const LEGRABOX_ORDER_OPTIONS = Object.freeze({ couleursChassis: Object.freeze([ 'blanc soie', 'argent polaire', 'gris orion', 'noir terra', 'acier inoxydable (anti-empreintes digitales)' ]), typesDeTiroir: Object.freeze([ 'Type de façade du tiroir (Standard)', 'Type de tiroir intérieur (Anglais)' ]), variantesOuvertureFermeture: Object.freeze(['Blumotion', 'Tip-On Blumotion', 'Tip-On']), fixationsFacadeDisponibles: Object.freeze(['Vis (Douille d\u2019écartement)', 'Expan-do T']), etatDeLivraison: Object.freeze(['F', 'C', 'S']) }); /** Règles métier strictes (regles_automatiques_requises). */ export const LEGRABOX_BOM_RULES = Object.freeze({ /** Ajouter « Stabilisation de façade » si largeur_corps_mm >= 600. */ frontStabiliserMinBodyWidthMm: 600, /** Ajouter « Unité de synchronisation » si variante Tip-On Blumotion. */ syncUnitVariant: 'tip_on_blumotion' }); /** * Longueurs nominales disponibles (mm) par système et capacité de charge. * N absent du dossier BOM — longueurs issues du catalogue Blum (40 kg uniquement). * Exception : K (hauteur L) indisponible en acier inoxydable. */ export const LEGRABOX_NOMINAL_LENGTHS_MM = Object.freeze({ N: Object.freeze({ 40: Object.freeze([400, 450, 500, 550]), 70: Object.freeze([]) }), M: Object.freeze({ 40: Object.freeze([270, 300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) }), K: Object.freeze({ 40: Object.freeze([300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600]) }), C: Object.freeze({ 40: Object.freeze([270, 300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) }), F: Object.freeze({ 40: Object.freeze([400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) }) }); /** Couleur châssis indisponible : hauteur K (L) en acier inoxydable. */ export function isLegraboxChassisColorAvailable(systemKey, chassisColor) { const isInox = String(chassisColor || '').toLowerCase().includes('inox'); return !(systemKey === 'K' && isInox); } /** * Plus grande longueur nominale disponible pour un système / capacité, bornée par la * profondeur utile (profondeur intérieure min = NL + 3 mm gérée par l'appelant). * @returns {number|null} */ export function getLegraboxNominalLengthMm(systemKey, availableDepthMm, loadKg = 40) { const table = LEGRABOX_NOMINAL_LENGTHS_MM[systemKey]; if (!table) return null; const list = table[Number(loadKg) === 70 ? 70 : 40] || []; const depth = Math.max(0, Number(availableDepthMm) || 0); let best = null; for (const nl of list) { if (nl <= depth && (best == null || nl > best)) best = nl; } return best; } // ── 6. Accessoires et subdivisions intérieures (up-sell) ──────────────── export const LEGRABOX_INTERIOR_ACCESSORIES = Object.freeze({ garnituresUniversellesPlastique: Object.freeze({ label: 'Garniture universelle plastique', nominalLengthCompatibleMm: 500, couleurs: Object.freeze(['Anthracite pearl', 'Blanc grainé']), modelesParHauteur: Object.freeze({ h40: Object.freeze({ hauteurMm: 40, largeursCorpsCompatiblesMm: Object.freeze([600, 800, 900]), note: 'Particulièrement adapté sous les champs d\u2019induction' }), h52: Object.freeze({ hauteurMm: 52, largeursCorpsCompatiblesMm: Object.freeze([500, 600, 800, 900]) }) }) }), assortimentsPorteCouverts: Object.freeze({ label: 'Assortiment porte-couverts modulaire', typeFixation: 'Autocollant avec insert en feutre', couleurs: Object.freeze(['Lie de vin', 'Anthracite']), configurations: Object.freeze([ Object.freeze({ pieces: 50, composition: '12 couteaux, 12 fourchettes, 12 cuillères, 12 cuillères à café, 2 grandes pièces' }), Object.freeze({ pieces: 70, composition: '12 couteaux, 12 fourchettes, 12 cuillères, 12 cuillères à café, 12 fourchettes de cuisine, 6 petites pièces, 4 grandes pièces' }), Object.freeze({ pieces: 90, composition: '24 couteaux, 24 fourchettes, 12 cuillères, 12 cuillères à café, 12 fourchettes de cuisine, 6 grandes pièces' }) ]) }), garnituresReglablesBoisInox: Object.freeze({ label: 'Garniture réglable hêtre / acier inox', finition: 'Hêtre / acier inox', nominalLengthCompatibleMm: 500, profondeurReelleMm: 478, modeles: Object.freeze({ bacEtroit: Object.freeze({ largeurCorpsCibleMm: [300, 400], largeurReelleAjustableMm: [200, 300] }), bacLarge: Object.freeze({ largeurCorpsCibleMm: [450, 700], largeurReelleAjustableMm: [350, 600] }) }) }), rangeCouvertsBoisHetre: Object.freeze({ label: 'Range-couverts réglable hêtre (laqué)', finition: 'Hêtre (laqué)', nominalLengthsCompatiblesMm: Object.freeze([500, 550, 600, 650]), /** Profondeur réelle = longueur nominale − 28 mm */ profondeurReelleDeductionMm: 28, modeles: Object.freeze({ bacEtroit: Object.freeze({ largeurCorpsCibleMm: [300, 400], largeurReelleAjustableMm: [200, 300] }), bacLarge: Object.freeze({ largeurCorpsCibleMm: [450, 700], largeurReelleAjustableMm: [350, 600] }) }) }) }); /** * Suggestions d'accessoires intérieurs compatibles avec la géométrie du tiroir. * @param {{ bodyWidthMm:number, nominalLengthMm:number }} params — largeur de corps (KB) et NL * @returns {{ key:string, label:string, detail:string }[]} */ export function getLegraboxAccessorySuggestions({ bodyWidthMm, nominalLengthMm }) { const kb = Math.max(0, Number(bodyWidthMm) || 0); const nl = Math.max(0, Number(nominalLengthMm) || 0); const out = []; const A = LEGRABOX_INTERIOR_ACCESSORIES; if (nl === A.garnituresUniversellesPlastique.nominalLengthCompatibleMm) { for (const [k, m] of Object.entries(A.garnituresUniversellesPlastique.modelesParHauteur)) { if (m.largeursCorpsCompatiblesMm.includes(kb)) { out.push({ key: `garniture_plastique_${k}`, label: `${A.garnituresUniversellesPlastique.label} h${m.hauteurMm} mm`, detail: m.note || `Largeur corps ${kb} mm, NL ${nl} mm` }); } } } const inRange = ([min, max]) => kb >= min && kb <= max; if (nl === A.garnituresReglablesBoisInox.nominalLengthCompatibleMm) { if (inRange(A.garnituresReglablesBoisInox.modeles.bacEtroit.largeurCorpsCibleMm)) { out.push({ key: 'garniture_bois_inox_etroit', label: `${A.garnituresReglablesBoisInox.label} — bac étroit`, detail: `Largeur réelle ajustable 200–300 mm` }); } if (inRange(A.garnituresReglablesBoisInox.modeles.bacLarge.largeurCorpsCibleMm)) { out.push({ key: 'garniture_bois_inox_large', label: `${A.garnituresReglablesBoisInox.label} — bac large`, detail: `Largeur réelle ajustable 350–600 mm` }); } } if (A.rangeCouvertsBoisHetre.nominalLengthsCompatiblesMm.includes(nl)) { const prof = nl - A.rangeCouvertsBoisHetre.profondeurReelleDeductionMm; if (inRange(A.rangeCouvertsBoisHetre.modeles.bacEtroit.largeurCorpsCibleMm)) { out.push({ key: 'range_couverts_hetre_etroit', label: `${A.rangeCouvertsBoisHetre.label} — bac étroit`, detail: `Profondeur réelle ${prof} mm` }); } if (inRange(A.rangeCouvertsBoisHetre.modeles.bacLarge.largeurCorpsCibleMm)) { out.push({ key: 'range_couverts_hetre_large', label: `${A.rangeCouvertsBoisHetre.label} — bac large`, detail: `Profondeur réelle ${prof} mm` }); } } return out; }