Files
apex/server/services/cuttingEngine.js
T
2026-02-13 00:21:53 +01:00

344 lines
10 KiB
JavaScript

/**
* 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 };