v1.0
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user