/** * Cutting Engine - Generate cutting list (liste de débitage) for wardrobe production * Calculates all panel dimensions based on configuration and color assignments */ // Panel thickness in mm const PANEL_THICKNESS = 19; const BACK_PANEL_THICKNESS = 8; // Edge banding settings const EDGE_BANDING = { visible: 2, // 2mm visible edge banding hidden: 0.5 // 0.5mm thin edge banding }; /** * Element dimensions calculator * Returns the dimensions for each element type based on wardrobe config */ function getElementDimensions(elementType, config, modules = []) { const { widthMm, heightMm, depthMm, plinthHeightMm, installationType } = config; const bodyHeight = heightMm - plinthHeightMm; const innerWidth = widthMm - (PANEL_THICKNESS * 2); const innerDepth = depthMm - PANEL_THICKNESS; // Back panel space const isNiche = installationType === 'niche'; switch (elementType) { case 'plintheBas': return { width: widthMm, height: plinthHeightMm, depth: depthMm, quantity: 1, description: 'Plinthe de base' }; case 'bandeauHaut': return { width: widthMm, height: PANEL_THICKNESS, depth: depthMm, quantity: 1, description: 'Bandeau supérieur' }; case 'dessus': return { width: innerWidth, height: PANEL_THICKNESS, depth: innerDepth, quantity: 1, description: 'Panneau du dessus' }; case 'fond': return { width: innerWidth, height: PANEL_THICKNESS, depth: innerDepth, quantity: 1, description: 'Panneau du fond (bas)' }; case 'cotes': return { width: PANEL_THICKNESS, height: bodyHeight, depth: depthMm, quantity: 2, description: 'Côtés extérieurs (gauche et droit)' }; case 'cotesDoublage': // Only for niche installation if (!isNiche) return null; return { width: PANEL_THICKNESS / 2, height: bodyHeight, depth: depthMm, quantity: 2, description: 'Côtés de doublage (niche)' }; case 'fileur': // Only for niche installation if (!isNiche) return null; return { width: PANEL_THICKNESS / 2, height: bodyHeight, depth: depthMm, quantity: 2, description: 'Fileurs (bouche-trou niche)' }; case 'dos': return { width: innerWidth, height: bodyHeight - (PANEL_THICKNESS * 2), depth: BACK_PANEL_THICKNESS, quantity: 1, description: 'Dos (panneau arrière)' }; case 'portes': const doorCount = modules.length > 0 ? modules.length : (config.blocks?.length || 2); const isSliding = config.doorType === 'sliding'; if (isSliding) { const panelCount = config.slidingPanelsCount || 2; const panelWidth = Math.ceil(widthMm / panelCount) + 50; // overlap return { width: panelWidth, height: bodyHeight - 20, // clearance depth: PANEL_THICKNESS, quantity: panelCount, description: `Portes coulissantes (${panelCount} vantaux)` }; } else { // Hinged doors - one per module/block return { width: Math.floor(widthMm / doorCount) - 4, // 2mm gap each side height: bodyHeight - 4, depth: PANEL_THICKNESS, quantity: doorCount, description: `Portes battantes (${doorCount} portes)` }; } case 'panneauxInterieurs': // Vertical separators between modules const separatorCount = modules.length > 1 ? modules.length - 1 : (config.blocks?.length || 1) - 1; if (separatorCount <= 0) return null; return { width: PANEL_THICKNESS, height: bodyHeight - (PANEL_THICKNESS * 2), depth: innerDepth, quantity: separatorCount, description: `Séparateurs verticaux (${separatorCount})` }; case 'etageres': // Count shelves from modules const totalShelves = modules.reduce((sum, m) => sum + (m.shelves || 0), 0); if (totalShelves <= 0) return null; // Average shelf width based on modules const avgModuleWidth = modules.length > 0 ? modules.reduce((sum, m) => sum + (m.widthMm || 0), 0) / modules.length : innerWidth; return { width: avgModuleWidth - (PANEL_THICKNESS * 2), height: PANEL_THICKNESS, depth: innerDepth - 20, // setback from front quantity: totalShelves, description: `Étagères (${totalShelves})` }; case 'champs': // Edge banding - calculated as linear meters // Include all visible edges const perimeter = (widthMm * 2) + (heightMm * 4) + (bodyHeight * 4); return { width: EDGE_BANDING.visible, height: perimeter, depth: 0, quantity: 1, unit: 'ml', description: 'Chant plaqué visible' }; case 'caisse': // The carcass/body - this is the assembled unit, not a cut piece return null; default: return null; } } /** * Get edge banding requirements for an element */ function getEdgeBanding(elementType) { const fullEdges = ['portes', 'cotes', 'dessus', 'plintheBas', 'bandeauHaut']; const partialEdges = ['etageres', 'panneauxInterieurs', 'fond']; if (fullEdges.includes(elementType)) { return { type: 'visible', sides: 4 }; } else if (partialEdges.includes(elementType)) { return { type: 'visible', sides: 1 }; // front edge only } else if (elementType === 'dos') { return { type: 'none', sides: 0 }; } return { type: 'hidden', sides: 2 }; } /** * Generate complete cutting list for a wardrobe configuration * @param {Object} config - Wardrobe configuration * @param {Object} colors - Color/finish assignments per element * @param {Array} modules - Module configurations (optional) * @returns {Object} Cutting list with pieces grouped by finish */ export function generateCuttingList(config, colors = {}, modules = []) { const pieces = []; // All element types to process const elementTypes = [ 'plintheBas', 'bandeauHaut', 'dessus', 'fond', 'cotes', 'cotesDoublage', 'fileur', 'dos', 'portes', 'panneauxInterieurs', 'etageres' ]; for (const elementType of elementTypes) { const dimensions = getElementDimensions(elementType, config, modules); if (!dimensions) continue; const finish = colors[elementType] || { finishId: null, label: 'Non défini' }; const edgeBanding = getEdgeBanding(elementType); pieces.push({ id: `piece-${elementType}`, elementType, description: dimensions.description, finishId: finish.finishId, finishLabel: finish.label, widthMm: Math.round(dimensions.width), heightMm: Math.round(dimensions.height), thicknessMm: dimensions.depth <= PANEL_THICKNESS ? dimensions.depth : PANEL_THICKNESS, quantity: dimensions.quantity, unit: dimensions.unit || 'pcs', edgeBanding: edgeBanding, surfaceM2: calculateSurface(dimensions.width, dimensions.height, dimensions.quantity) }); } // Calculate totals by finish const byFinish = {}; for (const piece of pieces) { const key = piece.finishLabel || 'Non défini'; if (!byFinish[key]) { byFinish[key] = { finishId: piece.finishId, finishLabel: key, pieces: [], totalSurfaceM2: 0, pieceCount: 0 }; } byFinish[key].pieces.push(piece); byFinish[key].totalSurfaceM2 += piece.surfaceM2; byFinish[key].pieceCount += piece.quantity; } // Calculate edge banding totals const edgeBandingMl = pieces.reduce((total, piece) => { if (piece.edgeBanding.type !== 'none') { const perimeter = (piece.widthMm + piece.heightMm) * 2 * piece.quantity / 1000; // convert to meters return total + (perimeter * piece.edgeBanding.sides / 4); } return total; }, 0); return { pieces, byFinish: Object.values(byFinish), summary: { totalPieces: pieces.reduce((sum, p) => sum + p.quantity, 0), totalSurfaceM2: pieces.reduce((sum, p) => sum + p.surfaceM2, 0), edgeBandingMl: Math.round(edgeBandingMl * 10) / 10, finishCount: Object.keys(byFinish).length }, config: { widthMm: config.widthMm, heightMm: config.heightMm, depthMm: config.depthMm, installationType: config.installationType, doorType: config.doorType } }; } /** * Calculate surface area in m² */ function calculateSurface(widthMm, heightMm, quantity = 1) { return Math.round((widthMm * heightMm * quantity) / 1000000 * 1000) / 1000; } /** * Format cutting list for display or export */ export function formatCuttingListForExport(cuttingList) { const lines = []; lines.push('LISTE DE DÉBITAGE'); lines.push('================='); lines.push(''); lines.push(`Configuration: ${cuttingList.config.widthMm}mm x ${cuttingList.config.heightMm}mm x ${cuttingList.config.depthMm}mm`); lines.push(`Type: ${cuttingList.config.installationType === 'niche' ? 'Encastrée' : 'Libre'}`); lines.push(`Portes: ${cuttingList.config.doorType === 'sliding' ? 'Coulissantes' : 'Battantes'}`); lines.push(''); lines.push('RÉSUMÉ'); lines.push('------'); lines.push(`Nombre total de pièces: ${cuttingList.summary.totalPieces}`); lines.push(`Surface totale: ${cuttingList.summary.totalSurfaceM2.toFixed(2)} m²`); lines.push(`Chant plaqué: ${cuttingList.summary.edgeBandingMl} ml`); lines.push(`Finitions différentes: ${cuttingList.summary.finishCount}`); lines.push(''); for (const group of cuttingList.byFinish) { lines.push(`FINITION: ${group.finishLabel}`); lines.push('-'.repeat(40)); lines.push('Pièce | Dim. (LxH) | Ép. | Qté | Surface'); for (const piece of group.pieces) { lines.push( `${piece.description.padEnd(30)} | ${piece.widthMm}x${piece.heightMm} | ${piece.thicknessMm}mm | x${piece.quantity} | ${piece.surfaceM2.toFixed(3)}m²` ); } lines.push(`Sous-total: ${group.pieceCount} pièces, ${group.totalSurfaceM2.toFixed(2)} m²`); lines.push(''); } return lines.join('\n'); } export default { generateCuttingList, formatCuttingListForExport };