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

359 lines
13 KiB
JavaScript

/**
* Plan Generator - Generate 2D assembly and parts plans in SVG format
* Also handles PDF export
*/
// Constants for drawing
const DRAWING_SCALE = 0.1; // 1mm = 0.1px
const MARGIN = 50;
const LABEL_OFFSET = 15;
const ARROW_SIZE = 8;
/**
* Generate SVG for assembly view (front elevation)
*/
export function generateAssemblyPlan(config, modules = []) {
const { widthMm, heightMm, plinthHeightMm, doorType, installationType } = config;
const bodyHeight = heightMm - plinthHeightMm;
const svgWidth = widthMm * DRAWING_SCALE + MARGIN * 2;
const svgHeight = heightMm * DRAWING_SCALE + MARGIN * 2;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
svg += `<style>
.outline { fill: none; stroke: #333; stroke-width: 1.5; }
.inner { fill: none; stroke: #666; stroke-width: 0.5; }
.dimension { fill: none; stroke: #0066cc; stroke-width: 0.5; }
.dim-text { font-family: Arial, sans-serif; font-size: 10px; fill: #0066cc; }
.label { font-family: Arial, sans-serif; font-size: 9px; fill: #333; }
.title { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
.plinth { fill: #e0d0c0; stroke: #333; stroke-width: 1; }
.body { fill: #f5e6d3; stroke: #333; stroke-width: 1; }
.door { fill: #d4c4a4; stroke: #333; stroke-width: 1; opacity: 0.8; }
.separator { stroke: #666; stroke-width: 0.5; stroke-dasharray: 4,2; }
</style>`;
// Title
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de face - Élévation</text>`;
const offsetX = MARGIN;
const offsetY = MARGIN;
// Plinth
const plinthY = offsetY + bodyHeight * DRAWING_SCALE;
svg += `<rect x="${offsetX}" y="${plinthY}" width="${widthMm * DRAWING_SCALE}" height="${plinthHeightMm * DRAWING_SCALE}" class="plinth" />`;
// Body
svg += `<rect x="${offsetX}" y="${offsetY}" width="${widthMm * DRAWING_SCALE}" height="${bodyHeight * DRAWING_SCALE}" class="body" />`;
// Module separators
if (modules.length > 0) {
let currentX = offsetX;
for (let i = 0; i < modules.length - 1; i++) {
currentX += modules[i].widthMm * DRAWING_SCALE;
svg += `<line x1="${currentX}" y1="${offsetY}" x2="${currentX}" y2="${plinthY}" class="separator" />`;
}
}
// Doors
if (doorType === 'sliding') {
const panelCount = config.slidingPanelsCount || 2;
const panelWidth = (widthMm / panelCount) * DRAWING_SCALE;
for (let i = 0; i < panelCount; i++) {
svg += `<rect x="${offsetX + i * panelWidth * 0.95 + 5}" y="${offsetY + 5}" width="${panelWidth * 0.9}" height="${(bodyHeight - 10) * DRAWING_SCALE}" class="door" />`;
}
} else {
const blockCount = modules.length > 0 ? modules.length : (config.blocks?.length || 2);
let currentX = offsetX;
const items = modules.length > 0 ? modules : (config.blocks || []);
for (let i = 0; i < blockCount; i++) {
const blockWidth = items[i]?.widthMm || (widthMm / blockCount);
svg += `<rect x="${currentX + 3}" y="${offsetY + 3}" width="${blockWidth * DRAWING_SCALE - 6}" height="${(bodyHeight - 6) * DRAWING_SCALE}" class="door" />`;
currentX += blockWidth * DRAWING_SCALE;
}
}
// Dimension lines
svg += generateDimensionLine(offsetX, plinthY + plinthHeightMm * DRAWING_SCALE + 20, widthMm * DRAWING_SCALE, `${widthMm} mm`, 'horizontal');
svg += generateDimensionLine(offsetX - 20, offsetY, bodyHeight * DRAWING_SCALE, `${bodyHeight} mm`, 'vertical');
svg += generateDimensionLine(offsetX - 40, offsetY, heightMm * DRAWING_SCALE, `${heightMm} mm (total)`, 'vertical');
svg += '</svg>';
return svg;
}
/**
* Generate SVG for side view
*/
export function generateSidePlan(config) {
const { heightMm, depthMm, plinthHeightMm } = config;
const bodyHeight = heightMm - plinthHeightMm;
const svgWidth = depthMm * DRAWING_SCALE + MARGIN * 2;
const svgHeight = heightMm * DRAWING_SCALE + MARGIN * 2;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
svg += `<style>
.outline { fill: none; stroke: #333; stroke-width: 1.5; }
.plinth { fill: #e0d0c0; stroke: #333; stroke-width: 1; }
.body { fill: #f5e6d3; stroke: #333; stroke-width: 1; }
.title { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
.dim-text { font-family: Arial, sans-serif; font-size: 10px; fill: #0066cc; }
</style>`;
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de côté</text>`;
const offsetX = MARGIN;
const offsetY = MARGIN;
// Plinth
const plinthY = offsetY + bodyHeight * DRAWING_SCALE;
svg += `<rect x="${offsetX}" y="${plinthY}" width="${depthMm * DRAWING_SCALE}" height="${plinthHeightMm * DRAWING_SCALE}" class="plinth" />`;
// Body
svg += `<rect x="${offsetX}" y="${offsetY}" width="${depthMm * DRAWING_SCALE}" height="${bodyHeight * DRAWING_SCALE}" class="body" />`;
// Dimension
svg += generateDimensionLine(offsetX, plinthY + plinthHeightMm * DRAWING_SCALE + 20, depthMm * DRAWING_SCALE, `${depthMm} mm`, 'horizontal');
svg += '</svg>';
return svg;
}
/**
* Generate SVG for top view
*/
export function generateTopPlan(config, modules = []) {
const { widthMm, depthMm } = config;
const svgWidth = widthMm * DRAWING_SCALE + MARGIN * 2;
const svgHeight = depthMm * DRAWING_SCALE + MARGIN * 2;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
svg += `<style>
.outline { fill: none; stroke: #333; stroke-width: 1.5; }
.body { fill: #f5e6d3; stroke: #333; stroke-width: 1; }
.separator { stroke: #666; stroke-width: 0.5; stroke-dasharray: 4,2; }
.title { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
.label { font-family: Arial, sans-serif; font-size: 8px; fill: #666; }
</style>`;
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de dessus</text>`;
const offsetX = MARGIN;
const offsetY = MARGIN;
// Body outline
svg += `<rect x="${offsetX}" y="${offsetY}" width="${widthMm * DRAWING_SCALE}" height="${depthMm * DRAWING_SCALE}" class="body" />`;
// Module separators and labels
if (modules.length > 0) {
let currentX = offsetX;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleWidth = module.widthMm * DRAWING_SCALE;
// Label
svg += `<text x="${currentX + moduleWidth / 2}" y="${offsetY + depthMm * DRAWING_SCALE / 2}" class="label" text-anchor="middle">${module.moduleCode || `M${i + 1}`}</text>`;
// Separator
if (i < modules.length - 1) {
currentX += moduleWidth;
svg += `<line x1="${currentX}" y1="${offsetY}" x2="${currentX}" y2="${offsetY + depthMm * DRAWING_SCALE}" class="separator" />`;
}
}
}
svg += '</svg>';
return svg;
}
/**
* Generate a dimension line with arrows and text
*/
function generateDimensionLine(x, y, length, label, direction = 'horizontal') {
let svg = '';
if (direction === 'horizontal') {
// Line
svg += `<line x1="${x}" y1="${y}" x2="${x + length}" y2="${y}" class="dimension" />`;
// Arrows
svg += `<polygon points="${x},${y} ${x + ARROW_SIZE},${y - 3} ${x + ARROW_SIZE},${y + 3}" fill="#0066cc" />`;
svg += `<polygon points="${x + length},${y} ${x + length - ARROW_SIZE},${y - 3} ${x + length - ARROW_SIZE},${y + 3}" fill="#0066cc" />`;
// Label
svg += `<text x="${x + length / 2}" y="${y + LABEL_OFFSET}" class="dim-text" text-anchor="middle">${label}</text>`;
} else {
// Line
svg += `<line x1="${x}" y1="${y}" x2="${x}" y2="${y + length}" class="dimension" />`;
// Arrows
svg += `<polygon points="${x},${y} ${x - 3},${y + ARROW_SIZE} ${x + 3},${y + ARROW_SIZE}" fill="#0066cc" />`;
svg += `<polygon points="${x},${y + length} ${x - 3},${y + length - ARROW_SIZE} ${x + 3},${y + length - ARROW_SIZE}" fill="#0066cc" />`;
// Label (rotated)
svg += `<text x="${x - LABEL_OFFSET}" y="${y + length / 2}" class="dim-text" text-anchor="middle" transform="rotate(-90 ${x - LABEL_OFFSET} ${y + length / 2})">${label}</text>`;
}
return svg;
}
/**
* Generate all plans for a configuration
*/
export function generateAllPlans(config, modules = [], colors = {}) {
return {
assembly: generateAssemblyPlan(config, modules),
side: generateSidePlan(config),
top: generateTopPlan(config, modules),
metadata: {
generatedAt: new Date().toISOString(),
config: {
widthMm: config.widthMm,
heightMm: config.heightMm,
depthMm: config.depthMm,
installationType: config.installationType,
doorType: config.doorType,
moduleCount: modules.length
}
}
};
}
/**
* Generate HTML page with all plans for PDF export
*/
export function generatePDFHtml(config, modules = [], cuttingList = null) {
const plans = generateAllPlans(config, modules);
let html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Plans Armoire</title>
<style>
@page { size: A4; margin: 15mm; }
body { font-family: Arial, sans-serif; font-size: 11px; }
.page { page-break-after: always; padding: 20px; }
.header { border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
.title { font-size: 20px; font-weight: bold; margin: 0; }
.subtitle { font-size: 12px; color: #666; margin: 5px 0 0; }
.plan-container { margin: 20px 0; text-align: center; }
.plan-title { font-size: 14px; font-weight: bold; margin-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
th { background: #f5f5f5; font-weight: bold; }
.summary { background: #f0f7ff; padding: 15px; border-radius: 8px; margin: 20px 0; }
.summary h3 { margin: 0 0 10px; }
.specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.spec-item { display: flex; justify-content: space-between; }
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1 class="title">Plans de fabrication - Armoire sur mesure</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="summary">
<h3>Spécifications</h3>
<div class="specs">
<div class="spec-item"><span>Largeur:</span><strong>${config.widthMm} mm</strong></div>
<div class="spec-item"><span>Hauteur:</span><strong>${config.heightMm} mm</strong></div>
<div class="spec-item"><span>Profondeur:</span><strong>${config.depthMm} mm</strong></div>
<div class="spec-item"><span>Plinthe:</span><strong>${config.plinthHeightMm} mm</strong></div>
<div class="spec-item"><span>Type:</span><strong>${config.installationType === 'niche' ? 'Encastrée' : 'Libre'}</strong></div>
<div class="spec-item"><span>Portes:</span><strong>${config.doorType === 'sliding' ? 'Coulissantes' : 'Battantes'}</strong></div>
</div>
</div>
<div class="plan-container">
<div class="plan-title">Vue de face (Élévation)</div>
${plans.assembly}
</div>
</div>
<div class="page">
<div class="plan-container">
<div class="plan-title">Vue de côté</div>
${plans.side}
</div>
<div class="plan-container">
<div class="plan-title">Vue de dessus</div>
${plans.top}
</div>
</div>`;
// Cutting list page
if (cuttingList) {
html += `
<div class="page">
<h2>Liste de débitage</h2>
<div class="summary">
<h3>Résumé</h3>
<div class="specs">
<div class="spec-item"><span>Nombre de pièces:</span><strong>${cuttingList.summary.totalPieces}</strong></div>
<div class="spec-item"><span>Surface totale:</span><strong>${cuttingList.summary.totalSurfaceM2.toFixed(2)} m²</strong></div>
<div class="spec-item"><span>Chant plaqué:</span><strong>${cuttingList.summary.edgeBandingMl} ml</strong></div>
<div class="spec-item"><span>Finitions:</span><strong>${cuttingList.summary.finishCount}</strong></div>
</div>
</div>`;
for (const group of cuttingList.byFinish) {
html += `
<h3>Finition: ${group.finishLabel}</h3>
<table>
<thead>
<tr>
<th>Description</th>
<th>Largeur</th>
<th>Hauteur</th>
<th>Ép.</th>
<th>Qté</th>
<th>Surface</th>
</tr>
</thead>
<tbody>`;
for (const piece of group.pieces) {
html += `
<tr>
<td>${piece.description}</td>
<td>${piece.widthMm} mm</td>
<td>${piece.heightMm} mm</td>
<td>${piece.thicknessMm} mm</td>
<td>${piece.quantity}</td>
<td>${piece.surfaceM2.toFixed(3)} m²</td>
</tr>`;
}
html += `
</tbody>
<tfoot>
<tr>
<th colspan="4">Sous-total</th>
<th>${group.pieceCount}</th>
<th>${group.totalSurfaceM2.toFixed(2)} m²</th>
</tr>
</tfoot>
</table>`;
}
html += `
</div>`;
}
html += `
</body>
</html>`;
return html;
}
export default {
generateAssemblyPlan,
generateSidePlan,
generateTopPlan,
generateAllPlans,
generatePDFHtml
};