/** * APEX 4.0 — Dossier Technique Maître * Single source of truth for all manufacturing specifications. * Every dimension in millimetres unless noted otherwise. */ // ── Panel thicknesses ──────────────────────────────────────────────── export const PANEL = { structure: 19, // Côtés, dessus, fond, séparateurs, étagères reinforcement: 16, // Renforts Forteresse back: 8, // Dos (panneau arrière) reinforcementWidth: 100, // Largeur standard d'un renfort }; // ── Reinforcement layout rules (Renforts Forteresse) ──────────────── export const REINFORCEMENT = { topBottomOffsetMm: 19, // Renfort haut/bas à 19 mm thirdRequiredAboveHeightMm: 1500, // 3e renfort si armoire > 1.5 m }; // ── Edge banding (Chants ABS) ──────────────────────────────────────── export const EDGE_BANDING = { abs: 2, // Chant ABS visible (2 mm) hidden: 0.5, // Chant fin caché (0.5 mm) backPanelSides: 2, // Dos: ABS sur 2 chants longs (haut + bas) topBottomSides: 2, // Dessus/Fond: ABS faces avant ET arrière shelfSides: 1, // Étagères: ABS face avant uniquement }; // ── Groove for back panel (Rainure dos) ────────────────────────────── export const GROOVE = { back: { width: 9, // mm — largeur rainure depth: 10.5, // mm — profondeur rainure axisFromPanelEdge: 16, // mm — rainure à 16 mm du bord des côtés/fond/dessus }, }; /** Rainure dos — plans / CNC : même coupe que montant (vertical) vs dessus/fond/bandeau (horizontal). */ export const BACK_GROOVE_PLAN_VERTICAL_TYPES = ['cotes', 'fileur', 'cotesDoublage']; export const BACK_GROOVE_PLAN_HORIZONTAL_TYPES = ['dessus', 'fond', 'bandeauHaut']; /** @returns {'vertical' | 'horizontal' | null} */ export function getBackGroovePlanOrientation(elementType) { if (BACK_GROOVE_PLAN_VERTICAL_TYPES.includes(elementType)) return 'vertical'; if (BACK_GROOVE_PLAN_HORIZONTAL_TYPES.includes(elementType)) return 'horizontal'; return null; } /** * Séparateurs : plaqués contre le dos (sans rainure). Largeur = même entraxe profondeur que le montant, * diminuée du décalage d’axe et de la demi-largeur de rainure (côtés). * @param {number} carcassDepthMm — profondeur caisse / largeur en plan du panneau côté (mm) */ export function getSeparatorDepthMm(carcassDepthMm) { const d = Number(carcassDepthMm) || 0; return Math.round(d - GROOVE.back.axisFromPanelEdge - GROOVE.back.width / 2); } // ── Connectors (Häfele Ixconnect) ──────────────────────────────────── export const CONNECTORS = { // SC 8/60 — structural assembly (caisse: côtés ↔ dessus/fond) ixconnect: { model: 'SC 8/60', edgeHole: { diameter: 8, depth: 45 }, // mm — perçage en about faceHole: { diameter: 8, depth: 12 }, // mm — perçage en face /** Trou de serrage Inbus — face intérieure panneaux horizontaux (dessus/fond) uniquement */ lockScrewInbus: { diameter: 6, depth: 9, /** mm — depuis le chant d’assemblage (axe du perçage Ø45), vers l’intérieur du panneau */ offsetFromAssemblyEdgeMm: 25 } }, // SC 8/25 — finish panels (portes, finitions) ixconnectFinish: { model: 'SC 8/25', faceHole: { diameter: 8, depth: 12.5 }, // mm — face interne Ø8 × 12.5mm axes: { back: 30, front: 43 }, // mm — axes: 30mm (arrière) / 43mm (avant) }, // Positioning per structural joint (axes mm depuis le bord avant de l’arête, le long de la profondeur) // — Arêtes d’extrémité (montants extérieurs): T → IX → T // — Jonctions internes (séparateurs): Ixconnect à l’axe médian profondeur, tourillons ±30 mm de cet axe jointPatternEdge: { tourillon1Axis: 20, ixconnectAxis: 50, tourillon2Axis: 80, }, /** @deprecated use jointPatternEdge — alias pour compat. */ jointPattern: { tourillon1Axis: 20, ixconnectAxis: 50, tourillon2Axis: 80, }, /** Tourillons par rapport à l’axe Ixconnect sur jonction milieu (mm). */ jointPatternMiddleTourillonOffsetFromIxMm: 30, tourillon: { diameter: 8, // mm — Ø tourillon length: 30, // mm — tourillon Ø8×30 }, tourillonDiameter: 8, // mm — backward compat }; /** * mm — depuis le bord physique panneau côté dos : axe rainure + largeur rainure (= fin utile côté dos, repère Ixconnect). * Évite chevauchement perçages / rainure dos sur dessus·fond. */ export function getBackGrooveInnerEndInsetFromBackEdgeMm() { return GROOVE.back.axisFromPanelEdge + GROOVE.back.width; } /** * Axes Ixconnect / tourillons le long de l’arête (profondeur caisse pour dessus·fond, largeur = profondeur pour côtés), * mesurés depuis le bord avant (face avant). * @param {'edge'|'middle'} variant — extrémité de caisse vs jonction entre modules (séparateur) * @param {{ horizontalPanel?: boolean }} [options] — si `horizontalPanel`, la profondeur utile exclut la zone rainure dos (16+9 mm depuis le bord arrière). */ export function getIxconnectJointPatternAlongDepthMm(depthMm, variant, options = {}) { const horizontalPanel = Boolean(options.horizontalPanel); const inset = horizontalPanel ? getBackGrooveInnerEndInsetFromBackEdgeMm() : 0; const d = Math.max(0, Number(depthMm) || 0); const dEff = horizontalPanel ? Math.max(0, d - inset) : d; const round1 = (v) => Math.round(v * 10) / 10; if (variant === 'edge') { const t1 = CONNECTORS.jointPatternEdge.tourillon1Axis; const ix = CONNECTORS.jointPatternEdge.ixconnectAxis; const t2 = CONNECTORS.jointPatternEdge.tourillon2Axis; if (!horizontalPanel || dEff <= 0) { return { role: 'edge', tourillon1Axis: t1, ixconnectAxis: ix, tourillon2Axis: t2 }; } if (dEff >= t2) { return { role: 'edge', tourillon1Axis: t1, ixconnectAxis: ix, tourillon2Axis: t2 }; } const s = dEff / t2; return { role: 'edge', tourillon1Axis: round1(t1 * s), ixconnectAxis: round1(ix * s), tourillon2Axis: round1(t2 * s) }; } if (dEff <= 0) { return { role: 'middle', tourillon1Axis: 0, ixconnectAxis: 0, tourillon2Axis: 0 }; } const ixMid = round1(dEff / 2); const off = CONNECTORS.jointPatternMiddleTourillonOffsetFromIxMm; return { role: 'middle', tourillon1Axis: round1(Math.max(0, ixMid - off)), ixconnectAxis: ixMid, tourillon2Axis: round1(Math.min(dEff, ixMid + off)) }; } /** * Largeurs de colonnes (mm) pour placer les jonctions — ordre: modules API, puis blocks, puis config.modules. * Évite le cas « modules = [] » alors que `config.modules` contient les largeurs (une seule jonction / mauvais comptage). */ function getModuleWidthsForJunctions(modules = [], config = {}) { const fromArr = (arr) => (Array.isArray(arr) ? arr : []) .map((m) => Number(m?.widthMm) || 0) .filter((v) => v > 0); let w = fromArr(modules); if (w.length === 0) w = fromArr(config.blocks); if (w.length === 0) w = fromArr(config.modules); return w; } /** * Positions X des jonctions verticales (côtés + séparateurs) sur un panneau horizontal — mm depuis le bord gauche. */ export function getJunctionAxesAcrossWidthMm(dimensionsWidthMm, modules = [], config = {}) { const maxW = Math.max(0, Number(dimensionsWidthMm) || 0); const mWidths = getModuleWidthsForJunctions(modules, config); if (mWidths.length > 0) { let acc = 0; const axes = [0]; for (let i = 0; i < mWidths.length - 1; i++) { acc += mWidths[i]; axes.push(Math.max(0, Math.min(maxW, acc))); } axes.push(maxW); const sorted = [...axes].sort((a, b) => a - b); const dedup = []; const eps = 0.5; for (const v of sorted) { if (dedup.length === 0 || Math.abs(v - dedup[dedup.length - 1]) > eps) { dedup.push(Math.round(v * 100) / 100); } } return dedup; } return [0, maxW]; } // ── System 32 drilling ─────────────────────────────────────────────── export const SYSTEM32 = { firstHoleOffset: 51, // mm — distance from panel top/bottom edge to first/last hole pitch: 32, // mm — distance between consecutive holes holeDiameter: 5, // mm — Ø 5 mm holeDepth: 12, // mm — profondeur de perçage (borgne, côtés de caisse) throughHole: false, // Côtés de caisse: perçages borgnes (non traversants) }; // ── System 32 for vertical separators ──────────────────────────────── export const SYSTEM32_SEPARATORS = { firstHoleOffset: 32, // mm — 51mm - 19mm (fond) = 32mm from bottom edge pitch: 32, // mm — same pitch holeDiameter: 5, // mm — Ø 5 mm throughHole: true, // Séparations verticales: Ø5mm TRAVERSANTS }; // ── Hinge positioning (Blum CLIP top) ──────────────────────────────── export const HINGE = { axisOffset: 103, // mm — per APEX 4.0 reversibility rule axisFrom: 'top_and_bottom', perDoor: 3, // default, real count uses matrix below reversible: true, // Panels are reversible (symmetric S32 drilling) model: 'Blum CLIP top', /** Per hinge cup: two Ø8 bores (Blum drilling pattern), depth mm, spacing mm (center–center), offset mm from cup center toward door interior (⟂ hinge edge). */ cupScrewDiameter: 8, cupScrewDepth: 13, cupScrewSpacing: 45, cupScrewOffsetFromCupCenter: 9.5, }; // Height/weight matrix from APEX 4.0 section 6.B export const HINGE_MATRIX = [ { maxHeightMm: 900, maxWeightKg: 6, hinges: 2 }, { maxHeightMm: 1600, maxWeightKg: 17, hinges: 3 }, { maxHeightMm: 2000, maxWeightKg: 20, hinges: 4 }, { maxHeightMm: 2400, maxWeightKg: 22, hinges: 5 }, { maxHeightMm: 2600, maxWeightKg: Infinity, hinges: 6 } ]; export function getHingeCountForDoor({ heightMm, weightKg = null }) { const h = Number(heightMm) || 0; const hasWeight = weightKg !== null && weightKg !== undefined && weightKg !== ''; const w = hasWeight && Number.isFinite(Number(weightKg)) ? Number(weightKg) : null; for (const row of HINGE_MATRIX) { const byHeight = h <= row.maxHeightMm; const byWeight = w == null ? false : w <= row.maxWeightKg; if (byHeight || byWeight) return row.hinges; } return HINGE_MATRIX[HINGE_MATRIX.length - 1].hinges; } export function getHingeCupPositions(doorHeightMm, hingeCount = null) { const height = Number(doorHeightMm) || 0; const count = Math.max(2, Number(hingeCount) || getHingeCountForDoor({ heightMm: height })); const first = HINGE.axisOffset; const last = Math.max(first, height - HINGE.axisOffset); if (count <= 2) return [first, last]; const spaces = count - 1; const rawStep = (last - first) / spaces; const positions = [first]; for (let i = 1; i < spaces; i++) { const rawPos = first + rawStep * i; // Rule: align intermediary hinges on System 32 grid. const snapped = Math.round(rawPos / 32) * 32; const minPos = positions[positions.length - 1] + 32; const maxPos = last - (spaces - i) * 32; positions.push(Math.min(Math.max(snapped, minPos), maxPos)); } positions.push(last); return positions; } // ── Plinth system (AXILO™ 78) ─────────────────────────────────────── export const PLINTH = { model: 'AXILO 78', footHeight: 78, // mm — hauteur pied réglable clipsPerMeter: 3, // clips de plinthe par mètre linéaire // Reserved packing space for hardware kit packingReserved: true, }; // ── Reinforcement machining (Renforts Forteresse) ──────────────────── export const REINFORCEMENT_MACHINING = { // Sur chaque petit chant d'extrémité: 1× Ixconnect SC 8/60 au centre + 2× tourillons à 20 mm des bords connectorModel: 'SC 8/60', connector: { model: 'SC 8/60', edgeHole: { diameter: 8, depth: 45 }, faceHole: { diameter: 8, depth: 12 }, }, ixconnectCenter: true, tourillonCount: 2, tourillonEdgeOffset: 20, // mm — tourillons à 20 mm des bords (axe mesuré depuis le même repère que l’Ixconnect) tourillonDiameter: 8, }; /** * Axes de perçage sur le petit chant d'extrémité (mm depuis le bord haut du renfort). * @param {number} reinforcementFaceHeightMm — hauteur de la face de renfort (généralement 100 mm) */ export function getReinforcementEndFixingAxesFromTopMm(reinforcementFaceHeightMm) { const h = Math.max(0, Number(reinforcementFaceHeightMm) || 0); const off = REINFORCEMENT_MACHINING.tourillonEdgeOffset; const round1 = (v) => Math.round(v * 10) / 10; return { // Deux tourillons proches des bords haut/bas de la face 100mm. tourillonAxesFromTopMm: [round1(off), round1(Math.max(off, h - off))], ixconnectAxisFromTopMm: round1(h / 2), }; } // ── Niche and sanitary gaps ───────────────────────────────────────────── export const NICHE = { sanitaryGapAround: 30, // mm — vide sanitaire de 3 cm tout autour drawerBackGap: 10, // mm — vide sanitaire derrière tiroir }; // ── Drawers (manufactured) ────────────────────────────────────────────── export const DRAWERS = { maxColumnWidth: 1000, // mm minDepth: 400, // mm maxDepth: 650, // mm depthStep: 50, // mm hiddenHandleDepthReduction: 40, // mm (assumption until final handle specs) standardHeights: [60, 90, 128, 177, 240], }; /** * Renforts latéraux caisse (25 mm) — uniquement si tiroirs cachés derrière porte ; * un montant gauche + un droit par colonne à tiroirs, même profondeur (min. des deux parois). * Fixation sur la face intérieure des montants / séparateurs. */ export const DRAWER_REINFORCEMENT = { thicknessMm: 25, /** mm depuis le fond caisse (repère rainure côté ou fond séparateur) — début utile du renfort */ backInsetFromRearMm: 25, /** mm réservés vers l’avant quand les tiroirs sont derrière une porte à charnières */ hingeFrontClearanceMm: 77, /** mm — déduit de la hauteur pile : partie droite au-dessus du rayon façade tiroir du haut */ topFrontRadiusMm: 3, /** Ixconnect « petit » — même famille que finitions SC 8/25 */ ixconnectModel: 'SC 8/25', /** mm — retrait des axes depuis chaque bord (chaque coin : 50 mm des deux côtés) */ ixconnectCornerInsetMm: 50, ixconnectHoleDiameterMm: 8, /** Perçage face côté renfort (vers montant / séparateur) */ ixconnectDepthInReinforcementMm: 13, /** Perçage face intérieure montant ou séparateur (vis-à-vis) */ ixconnectDepthInVerticalPanelMm: 12, }; /** * Ixconnect SC 8/25 sur montants / séparateurs — zone tiroirs cachés + renfort. * Repère face intérieure : X depuis bord avant (profondeur), Y depuis bas du panneau (corps). */ export const DRAWER_RENFORT_PANEL_IXCONNECT = { model: 'SC 8/25', /** 77 + 50 mm depuis le devant (axe avant) */ frontAxesFromFrontMm: 77 + 50, /** Côté caisse : 19 + 9 + 50 mm depuis le fond */ backAxesFromRearCoteMm: 19 + 9 + 50, /** Séparateur : 50 mm depuis le fond */ backAxesFromRearSeparatorMm: 50, /** 50 mm du haut / bas de la zone renfort */ verticalInsetMm: 50, faceHoleDiameterMm: 8, faceHoleDepthMm: 12 }; /** * 4 trous (rectangle) sur une face intérieure montant/séparateur pour renfort tiroir caché. * @param {number} panelDepthMm — largeur du panneau en plan (= profondeur caisse ou prof. séparateur) * @param {boolean} isCote — true = montant extérieur, false = séparateur * @param {number} zoneBottomFromPanelBottomMm — bas de la zone tiroirs depuis bas du panneau * @param {number} renfortHeightMm — hauteur utile renfort (= pile tiroirs − R) */ export function buildDrawerRenfortPanelIxconnectCornersMm( panelDepthMm, isCote, zoneBottomFromPanelBottomMm, renfortHeightMm ) { const P = DRAWER_RENFORT_PANEL_IXCONNECT; const D = Math.max(0, Number(panelDepthMm) || 0); const z0 = Math.max(0, Number(zoneBottomFromPanelBottomMm) || 0); const h = Math.max(0, Number(renfortHeightMm) || 0); const backInset = isCote ? P.backAxesFromRearCoteMm : P.backAxesFromRearSeparatorMm; const xf = P.frontAxesFromFrontMm; const xb = Math.max(xf + 1, D - backInset); const y1 = z0 + P.verticalInsetMm; const y2 = z0 + h - P.verticalInsetMm; if (h < 2 * P.verticalInsetMm + 5 || D < xf + backInset + 5 || y2 <= y1) return []; return [ { xMm: xf, yMm: y1, role: 'avant_bas' }, { xMm: xf, yMm: y2, role: 'avant_haut' }, { xMm: xb, yMm: y1, role: 'fond_bas' }, { xMm: xb, yMm: y2, role: 'fond_haut' } ]; } /** * 4 trous d’angle pour Ixconnect SC 8/25 — repère plan face : X depuis bord gauche, Y depuis bord haut (mm). * @returns {{ xMm: number, yMm: number, role: string }[]} */ export function getDrawerReinforcementIxconnectHolePositionsMm(widthMm, heightMm, insetMm = DRAWER_REINFORCEMENT.ixconnectCornerInsetMm) { const w = Math.max(0, Number(widthMm) || 0); const h = Math.max(0, Number(heightMm) || 0); const ins = Math.max(8, Math.min(Number(insetMm) || 50, Math.min(w, h) / 2 - 0.5)); if (w < 2 * ins || h < 2 * ins) return []; return [ { xMm: ins, yMm: ins, role: 'nw' }, { xMm: w - ins, yMm: ins, role: 'ne' }, { xMm: ins, yMm: h - ins, role: 'sw' }, { xMm: w - ins, yMm: h - ins, role: 'se' } ]; } /** * Hauteur totale du bloc tiroirs (façades + jeux), mm. * @param {number} drawerCount * @param {number[]} drawerHeightsMm * @param {number} [drawerGapMm] */ export function drawerStackHeightMm(drawerCount, drawerHeightsMm, drawerGapMm = 8) { const n = Math.max(0, Number(drawerCount) || 0); if (n <= 0) return 0; const heights = Array.isArray(drawerHeightsMm) && drawerHeightsMm.length ? drawerHeightsMm : DRAWERS.standardHeights; const gap = Number.isFinite(Number(drawerGapMm)) ? Number(drawerGapMm) : 8; const sum = heights .slice(0, Math.min(n, heights.length)) .reduce((s, v) => s + (Number(v) || 0), 0); return Math.max(0, Math.round(sum + Math.max(0, n - 1) * gap)); } // ── Full-height handle assumptions ────────────────────────────────────── export const HANDLE = { /** Retrait mécanique rainure poignée (haut + bas) */ fullHeightTopBottomOffset: 2, // mm from top and bottom /** Déduction largeur panneau porte (vs poignée standard) — même valeur liste de débitage / plans */ fullHeightWidthDeductionMm: 18, /** Rainure usinée sur le montant côté poignée (profil type dos, à confirmer au profilé) */ fullHeightGroove: { width: GROOVE.back.width, depth: GROOVE.back.depth, }, }; /** * Côté charnières en élévation pour la porte d’index `doorIndex` : * vantail extérieur gauche → charnières gauche ; droit → droite ; portes centrales → réglage utilisateur. */ export function getDoorHingeSide(doorIndex, doorCount, hardware = {}) { const hw = hardware || {}; const n = Math.max(1, Number(doorCount) || 1); const i = Math.max(0, Number(doorIndex) || 0); if (n <= 1) return hw.doorHingeSide === 'right' ? 'right' : 'left'; if (i === 0) return 'left'; if (i === n - 1) return 'right'; return hw.centerDoorHingeSide === 'right' ? 'right' : 'left'; } // ── Block assembly drilling ───────────────────────────────────────────── export const BLOCK_ASSEMBLY = { sideOffsetFromFace: 30, // mm sideOffsetFromBack: 30, // mm topBottomOffset: 100, // mm middleCount: 3, // per joint }; // ── Carcass assembly ferrures ─────────────────────────────────────────── export const CARCASS_FASTENERS = { /** Ferrure d'assemblage utilisée pour les jonctions de caisse (côtés/séparateurs ↔ dessus/fond). */ model: 'Häfele Rasant-Tab', /** Référence produit (Ferrures d’assemblage Rasant-Tab, Ø trou 3 ou 5 mm). */ productUrl: 'https://www.haefele.ch/fr/produkt/ferrures-d-assemblage-rasant-tab-visser-dans-trou-de-per-age-3-ou-5-mm/26236520/?MasterSKU=P-00861456', piecesPerJoint: 3, /** Retrait des ferrures d’extrémité (mm) */ endInsetMm: 50, edgeOffsets: [50, 'middle', 50], /** Perçage ferrure: profondeur sur longueur de la pièce (mm) */ ferrureDepthAlongLengthMm: 46, /** Perçage ferrure: profondeur sur l’épaisseur de la pièce (mm) */ ferrureDepthInThicknessMm: 12, dowel: { diameter: 8, /** Longueur nominale tourillon (réf. atelier) */ depth: 40, /** Perçage borgne en face (dans l’épaisseur du panneau), mm — toujours 12 pour dessus/fond */ faceDrillDepthMm: 12, /** 2× tourillons par ferrure : axes à ±sideOffset du centre de chaque ferrure (mm) */ sideOffset: 30, }, }; /** Profondeur effective perçage tourillon en face (ne dépasse pas l’épaisseur). */ export function tourillonFaceDrillDepthMm(thicknessMm) { const cap = CARCASS_FASTENERS.dowel.faceDrillDepthMm; const t = Math.max(1, Number(thicknessMm) || PANEL.structure); return Math.min(cap, Math.max(6, t - 2)); } /** Longueur mini. d’arête (mm) pour placer une ferrure au milieu (en plus des 50 mm d’extrémité). */ export const CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM = 500; /** * Centres des ferrures le long d’une arête de jonction. * Règle: 50 mm des extrémités ; ferrure centrale seulement si l’arête dépasse {@link CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM}. * @param {number} edgeLengthMm — longueur de l’arête (mm), ici hauteur utile de la jonction * @returns {number[]} distances depuis le début de l’arête (mm) */ export function getCarcassJointPositions(edgeLengthMm) { const L = Math.max(0, Number(edgeLengthMm) || 0); if (L <= 0) return []; const inset = Math.min(CARCASS_FASTENERS.endInsetMm, Math.max(20, L * 0.1)); const tail = L - inset; const minMid = CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM; if (tail <= inset + 20) { if (L > minMid) { const u = L / 4; return [u, L / 2, L - u].map((v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10); } const u = L / 4; return [u, L - u].map((v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10); } if (L > minMid) { return [inset, L / 2, tail].map((v) => Math.round(v * 10) / 10); } return [inset, tail].map((v) => Math.round(v * 10) / 10); } /** * Chaque ferrure: 3 perçages alignés sur la longueur: * - 1 perçage ferrure au centre * - 2 perçages tourillons à ±sideOffset du centre * @returns {{ centerMm: number, holesAlongMm: [number, number, number] }[]} */ export function getCarcassFerrureDowelDrillings(centerPositionsMm, edgeLengthMm, sideOffsetMm = CARCASS_FASTENERS.dowel.sideOffset) { const L = Math.max(0, Number(edgeLengthMm) || 0); const off = Math.max(0, Number(sideOffsetMm) || 0); const centers = centerPositionsMm || []; const clamp = (v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10; return centers.map((c0) => { const c = Number(c0); return { centerMm: c, holesAlongMm: [clamp(c - off), clamp(c), clamp(c + off)] }; }); } // ── Shelf machining ───────────────────────────────────────────────── export const SHELF_MACHINING = { // Tourillons Ø8 front + back for shelf pin alignment tourillonDiameter: 8, // mm tourillonCount: 4, // 2 front + 2 back per shelf // "2 rayons sur une même strate de 19mm" — 2 shelves can share same S32 row shelvesPerStratum: 2, }; // ── Packing ────────────────────────────────────────────────────────── export const PACKING = { maxWeightKg: 28, // Approximate density for weight estimation (kg per m³) densityKgM3: { melamine19: 680, // Panneau mélaminé 19 mm melamine16: 660, // Panneau mélaminé 16 mm (renforts) melamine25: 670, // Panneau mélaminé 25 mm (renforts tiroir) melamine8: 640, // Panneau mélaminé 8 mm (dos) }, // Layer-based packing strategy (per APEX 4.0 spec): layers: { level1: 'sides', // Niveau 1: Côtés empilés level2: 'topAndBottom', // Niveau 2: Dessus + fond côte à côte level3: 'shelvesAndRenforts', // Niveau 3: Étagères + renforts groupés par épaisseur }, // Hardware kit reservation (AXILO™ pieds + clips de plinthe) hardwareKitReserved: true, // Espace réservé dans le colisage }; // ── Convenience: convert mm constants to metres (for Three.js) ────── export const PANEL_M = { structure: PANEL.structure / 1000, reinforcement: PANEL.reinforcement / 1000, back: PANEL.back / 1000, reinforcementWidth: PANEL.reinforcementWidth / 1000, };