/** * Plan Generator - Generate 2D assembly and parts plans in SVG format * Includes APEX 4.0 manufacturing details: System 32 drilling, hinge * positions, Ixconnect connectors, back panel groove, and reinforcements. * Also handles PDF export. */ import { PANEL, GROOVE, SYSTEM32, SYSTEM32_SEPARATORS, HINGE, CONNECTORS, getHingeCountForDoor, getHingeCupPositions, getDoorHingeSide, HANDLE, EDGE_BANDING, REINFORCEMENT, REINFORCEMENT_MACHINING, PLINTH, SHELF_MACHINING } from '../../shared/manufacturing.js'; // Constants for drawing const DRAWING_SCALE = 0.1; // 1mm = 0.1px const MARGIN = 50; const LABEL_OFFSET = 15; const ARROW_SIZE = 8; // ── Shared SVG styles ──────────────────────────────────────────────── const SHARED_STYLES = ` .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; } .s32-hole { fill: none; stroke: #e63946; stroke-width: 0.4; } .hinge-marker { fill: #ff6b35; stroke: #333; stroke-width: 0.3; } .groove { fill: none; stroke: #2a9d8f; stroke-width: 1; stroke-dasharray: 2,2; } .connector { fill: #6c5ce7; stroke: #333; stroke-width: 0.3; } .renfort { fill: #c9a96e; stroke: #333; stroke-width: 0.5; opacity: 0.7; } .detail-label { font-family: Arial, sans-serif; font-size: 7px; fill: #666; } .fh-groove { fill: none; stroke: #0d9488; stroke-width: 1; stroke-dasharray: 3, 2; } `; // ── Helpers ────────────────────────────────────────────────────────── /** * Draw System 32 hole column (vertical series of holes) */ function drawSystem32Holes(x, topY, panelHeightMm, scale) { let svg = ''; const usable = panelHeightMm - SYSTEM32.firstHoleOffset; const count = Math.max(0, Math.floor(usable / SYSTEM32.pitch) + 1); const r = (SYSTEM32.holeDiameter / 2) * scale; for (let i = 0; i < count; i++) { const yOffset = (SYSTEM32.firstHoleOffset + i * SYSTEM32.pitch) * scale; svg += ``; } return svg; } /** * Draw hinge position markers on a door plan (côté charnières) */ function drawHingeMarkers(doorX, doorY, doorWidthMm, doorHeightMm, scale, hingeSide = 'left') { let svg = ''; const markerH = 6; const markerW = 3; const hingeCount = getHingeCountForDoor({ heightMm: doorHeightMm }); const positions = getHingeCupPositions(doorHeightMm, hingeCount); const doorW = doorWidthMm * scale; const cupInset = Math.min(22 * scale, doorW * 0.13); const hingeLineX = hingeSide === 'right' ? doorX + doorW - cupInset : doorX + cupInset; for (const posMm of positions) { const y = doorY + posMm * scale; svg += ``; } return svg; } /** * Rainure poignée toute hauteur — côté opposé aux charnières, retrait H/B (HANDLE.fullHeightTopBottomOffset) */ function drawFullHeightHandleGrooveElevation(doorX, doorY, doorWidthMm, doorHeightMm, scale, hingeSide) { const off = HANDLE.fullHeightTopBottomOffset; const gw = HANDLE.fullHeightGroove.width; const doorW = doorWidthMm * scale; const axisX = hingeSide === 'right' ? doorX + (gw / 2) * scale : doorX + doorW - (gw / 2) * scale; const y1 = doorY + off * scale; const y2 = doorY + doorHeightMm * scale - off * scale; let svg = ``; const tx = axisX + (hingeSide === 'right' ? 5 : -5); const anchor = hingeSide === 'right' ? 'start' : 'end'; svg += `Rainure poignée TH (${gw} mm) · ${off} mm H/B`; return svg; } /** * Draw back panel groove indicator (dashed line near panel edge). * Per updated spec: groove at 16mm from panel edge. */ function drawGrooveIndicator(x, y, length, direction, scale) { let svg = ''; const grooveOffset = GROOVE.back.axisFromPanelEdge * scale; if (direction === 'vertical') { svg += ``; } else { svg += ``; } return svg; } /** * Generate a dimension line with arrows and text */ function generateDimensionLine(x, y, length, label, direction = 'horizontal') { let svg = ''; if (direction === 'horizontal') { svg += ``; svg += ``; svg += ``; svg += `${label}`; } else { svg += ``; svg += ``; svg += ``; svg += `${label}`; } return svg; } // ── Plan generators ────────────────────────────────────────────────── /** * Generate SVG for assembly view (front elevation) */ export function generateAssemblyPlan(config, modules = []) { const { widthMm, heightMm, plinthHeightMm, doorType } = config; const bodyHeight = heightMm - plinthHeightMm; const S = DRAWING_SCALE; const svgWidth = widthMm * S + MARGIN * 2; const svgHeight = heightMm * S + MARGIN * 2; let svg = ``; svg += ``; // Title svg += `Vue de face - Élévation`; const oX = MARGIN; const oY = MARGIN; // Plinth const plinthY = oY + bodyHeight * S; svg += ``; // Body svg += ``; // Module separators if (modules.length > 0) { let currentX = oX; for (let i = 0; i < modules.length - 1; i++) { currentX += modules[i].widthMm * S; svg += ``; } } // System 32 holes on left and right side panels svg += drawSystem32Holes(oX + PANEL.structure * S / 2, oY + PANEL.structure * S, bodyHeight - PANEL.structure * 2, S); svg += drawSystem32Holes(oX + widthMm * S - PANEL.structure * S / 2, oY + PANEL.structure * S, bodyHeight - PANEL.structure * 2, S); // Doors if (doorType === 'sliding') { const panelCount = config.slidingPanelsCount || 2; const panelWidth = (widthMm / panelCount) * S; for (let i = 0; i < panelCount; i++) { svg += ``; } } else { const blockCount = modules.length > 0 ? modules.length : (config.blocks?.length || 2); let currentX = oX; const items = modules.length > 0 ? modules : (config.blocks || []); const hw = config.hardware || {}; const fullHeightHandle = hw.handleType === 'full_height'; const widthDeductionMm = fullHeightHandle ? HANDLE.fullHeightWidthDeductionMm : 0; const gapMm = 2; const doorHmm = bodyHeight - 4; for (let i = 0; i < blockCount; i++) { const bw = items[i]?.widthMm || (widthMm / blockCount); const doorWmm = Math.max(100, Math.floor(widthMm / blockCount) - 4 - widthDeductionMm); const modulePx = bw * S; const doorWPx = doorWmm * S; const doorX = currentX + (modulePx - doorWPx) / 2; const doorY = oY + gapMm * S; svg += ``; const hingeSide = getDoorHingeSide(i, blockCount, hw); svg += drawHingeMarkers(doorX, doorY, doorWmm, doorHmm, S, hingeSide); if (fullHeightHandle) { svg += drawFullHeightHandleGrooveElevation(doorX, doorY, doorWmm, doorHmm, S, hingeSide); } currentX += modulePx; } } // Reinforcements (Forteresse 16 mm): // width = armoire width - 2×19 (entre côtés), // one at top + one at bottom (offset 19 mm), // third one centered if wardrobe height > 1500 mm. const renfortX = oX + PANEL.structure * S; const renfortW = Math.max(0, (widthMm - PANEL.structure * 2) * S); const renfortH = PANEL.reinforcement * S; const topRenfortY = oY + REINFORCEMENT.topBottomOffsetMm * S; const bottomRenfortY = plinthY - REINFORCEMENT.topBottomOffsetMm * S - renfortH; const reinforcementYs = [topRenfortY, bottomRenfortY]; if (heightMm > REINFORCEMENT.thirdRequiredAboveHeightMm) { reinforcementYs.push(oY + (bodyHeight * S) / 2 - renfortH / 2); } for (const y of reinforcementYs) { svg += ``; } svg += `Renforts Forteresse (16 mm) : fixation par montant — ${REINFORCEMENT_MACHINING.connectorModel} au centre + 2× tourillons Ø${REINFORCEMENT_MACHINING.tourillonDiameter} à ${REINFORCEMENT_MACHINING.tourillonEdgeOffset} mm des bords · axes sur plan pièce « Renforts »`; // Dimension lines svg += generateDimensionLine(oX, plinthY + plinthHeightMm * S + 20, widthMm * S, `${widthMm} mm`, 'horizontal'); svg += generateDimensionLine(oX - 20, oY, bodyHeight * S, `${bodyHeight} mm`, 'vertical'); svg += generateDimensionLine(oX - 40, oY, heightMm * S, `${heightMm} mm (total)`, 'vertical'); svg += ''; return svg; } /** * Generate SVG for side view — includes groove indicator and System 32 holes */ export function generateSidePlan(config) { const { heightMm, depthMm, plinthHeightMm } = config; const bodyHeight = heightMm - plinthHeightMm; const S = DRAWING_SCALE; const svgWidth = depthMm * S + MARGIN * 2; const svgHeight = heightMm * S + MARGIN * 2; let svg = ``; svg += ``; svg += `Vue de côté`; const oX = MARGIN; const oY = MARGIN; // Plinth const plinthY = oY + bodyHeight * S; svg += ``; // Body svg += ``; // Back panel (8mm) shown as a thin rectangle at the back const backX = oX + (depthMm - PANEL.back) * S; svg += ``; svg += `Dos ${PANEL.back}mm`; // Groove indicator (dashed green line at 16mm from panel edge) const grooveAxisX = oX + (depthMm - GROOVE.back.axisFromPanelEdge) * S; svg += ``; svg += `Rainure ${GROOVE.back.width}×${GROOVE.back.depth} (16mm du bord)`; // System 32 holes along the side panel svg += drawSystem32Holes(oX + 37 * S, oY + PANEL.structure * S, bodyHeight - PANEL.structure * 2, S); svg += `System 32`; // Dimension lines svg += generateDimensionLine(oX, plinthY + plinthHeightMm * S + 20, depthMm * S, `${depthMm} mm`, 'horizontal'); svg += generateDimensionLine(oX - 20, oY, bodyHeight * S, `${bodyHeight} mm`, 'vertical'); svg += ''; return svg; } /** * Generate SVG for top view — includes groove line for back panel */ export function generateTopPlan(config, modules = []) { const { widthMm, depthMm } = config; const S = DRAWING_SCALE; const svgWidth = widthMm * S + MARGIN * 2; const svgHeight = depthMm * S + MARGIN * 2; let svg = ``; svg += ``; svg += `Vue de dessus`; const oX = MARGIN; const oY = MARGIN; // Body outline svg += ``; // Back panel groove line (horizontal dashed line near panel edge) const grooveY = oY + (depthMm - PANEL.structure) * S; svg += ``; svg += `Rainure dos`; // Module separators and labels if (modules.length > 0) { let currentX = oX; for (let i = 0; i < modules.length; i++) { const module = modules[i]; const moduleWidth = module.widthMm * S; // Label svg += `${module.moduleCode || `M${i + 1}`}`; // Separator if (i < modules.length - 1) { currentX += moduleWidth; svg += ``; } } } svg += ''; return svg; } /** * Generate SVG detail plan — zoomed view of a side panel showing * System 32 drilling, connector holes, groove, and edge banding */ export function generateDetailPlan(config) { const { depthMm, heightMm, plinthHeightMm } = config; const bodyHeight = heightMm - plinthHeightMm; const innerWidth = config.widthMm - (PANEL.structure * 2); const innerDepth = depthMm - PANEL.structure; // Zoomed scale for detail view (3× normal) const DS = 0.3; // Show top 600mm of a side panel (enough for connectors, first S32 holes, groove) const detailHeightMm = Math.min(600, bodyHeight); const detailWidthMm = depthMm; // Extra space on the right for shelf drilling inset const svgWidth = detailWidthMm * DS + MARGIN * 2 + 260; const svgHeight = detailHeightMm * DS + MARGIN * 2 + 40; let svg = ``; svg += ``; svg += `Détail — Côté (vue intérieure)`; const oX = MARGIN + 40; const oY = MARGIN + 20; // Panel outline svg += ``; // Edge banding (2mm ABS on all 4 edges) const eb = EDGE_BANDING.abs * DS; svg += ``; // top svg += ``; // bottom svg += ``; // left svg += ``; // right // Back panel groove — 16mm from panel edge const grooveAxisOffset = (detailWidthMm - GROOVE.back.axisFromPanelEdge) * DS; const grooveX = oX + grooveAxisOffset - (GROOVE.back.width / 2) * DS; const grooveW = GROOVE.back.width * DS; svg += ``; // Callout svg += ``; svg += `Rainure ${GROOVE.back.width}×${GROOVE.back.depth} mm`; svg += `Position à ${GROOVE.back.axisFromPanelEdge} mm du bord`; // System 32 holes (front row) — blind holes, depth 12mm const s32X = oX + 37 * DS; // 37mm from front edge const usable = detailHeightMm - SYSTEM32.firstHoleOffset * 2; // first AND last at 51mm const holeCount = Math.max(0, Math.floor(usable / SYSTEM32.pitch) + 1); const r = (SYSTEM32.holeDiameter / 2) * DS; for (let i = 0; i < Math.min(holeCount, 15); i++) { const holeY = oY + (SYSTEM32.firstHoleOffset + i * SYSTEM32.pitch) * DS; svg += ``; } // Callout for System 32 svg += ``; svg += `System 32 (borgne)`; svg += `Ø${SYSTEM32.holeDiameter} / prof. ${SYSTEM32.holeDepth} / pas ${SYSTEM32.pitch}`; // First hole offset dimension svg += generateDimensionLine(oX + detailWidthMm * DS + 10, oY, SYSTEM32.firstHoleOffset * DS, `${SYSTEM32.firstHoleOffset}`, 'vertical'); // Ixconnect connector holes (top edge) — extrémité d’arête: T@20 → IX@50 → T@80 (vue détail côté) const conn = CONNECTORS.ixconnect; const pattern = CONNECTORS.jointPattern; // Show 3 holes: tourillon@20, ixconnect@50, tourillon@80 from left edge (arête extrémité) const jT1X = oX + pattern.tourillon1Axis * DS; const jIXX = oX + pattern.ixconnectAxis * DS; const jT2X = oX + pattern.tourillon2Axis * DS; const tourR = (CONNECTORS.tourillonDiameter / 2) * DS; const ixEdgeR = (conn.edgeHole.diameter / 2) * DS; // Edge holes on top edge svg += ``; // tourillon 1 svg += ``; // Ixconnect svg += ``; // tourillon 2 // Face holes (Ixconnect only — Ø8×12mm) svg += ``; // Callout svg += ``; svg += `Ixconnect ${conn.model}`; svg += `About: Ø${conn.edgeHole.diameter}×${conn.edgeHole.depth} / Face: Ø${conn.faceHole.diameter}×${conn.faceHole.depth}`; svg += `Pattern: T@${pattern.tourillon1Axis} → IX@${pattern.ixconnectAxis} → T@${pattern.tourillon2Axis}`; // Panel thickness label svg += `Épaisseur: ${PANEL.structure} mm — Chant ABS ${EDGE_BANDING.abs} mm`; // --- Inset: Rayons mobiles (Étagères amovibles) — SP 15/8/5 --- // Dimensions de découpe : L.int - 2 mm / P.int - 5 mm const shelfWmm = Math.max(80, innerWidth - 2); const shelfDmm = Math.max(80, innerDepth - 5); const insetMaxWpx = 170; const insetMaxHpx = 190; const insetScale = Math.min(insetMaxWpx / shelfWmm, insetMaxHpx / shelfDmm); const insetX = oX + detailWidthMm * DS + 60; const insetY = oY + 40; const insetW = shelfWmm * insetScale; const insetH = shelfDmm * insetScale; svg += `Rayons mobiles — SP 15/8/5 (dessous)`; svg += ``; // Hole positions: from front 32mm; from back 37mm const yFrontMm = 32; const yBackMm = shelfDmm - 37; const yPosMm = [yFrontMm, yBackMm].filter((v, idx, arr) => arr.findIndex((u) => Math.abs(u - v) < 1e-6) === idx); // Face holes: Ø20 axis 9.5mm from left/right const xSideMm = 9.5; const xPosMm = [xSideMm, shelfWmm - xSideMm].filter((v, idx, arr) => arr.findIndex((u) => Math.abs(u - v) < 1e-6) === idx); const rFace = (20 / 2) * insetScale; const rEdge = (8 / 2) * insetScale; for (const yMm of yPosMm) { const cy = insetY + yMm * insetScale; // Face holes (underside) — left/right for (const xMm of xPosMm) { const cx = insetX + xMm * insetScale; svg += ``; } // Edge holes (against side panels) — left/right edge svg += ``; svg += ``; } // Callouts svg += ``; svg += `Face: Ø20×15 — axe 9.5 mm`; svg += `Chant: Ø8×37 — à 6 mm du dessous`; svg += `Avant: 32 mm · Arrière: 37 mm`; svg += ''; 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), detail: generateDetailPlan(config), metadata: { generatedAt: new Date().toISOString(), specs: 'APEX 4.0', 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, packingList = null) { const plans = generateAllPlans(config, modules); let html = ` Plans Armoire — APEX 4.0

Plans de fabrication - Armoire sur mesure

APEX 4.0 — Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}

Spécifications

Largeur:${config.widthMm} mm
Hauteur:${config.heightMm} mm
Profondeur:${config.depthMm} mm
Plinthe:${config.plinthHeightMm} mm
Type:${config.installationType === 'niche' ? 'Encastrée' : 'Libre'}
Portes:${config.doorType === 'sliding' ? 'Coulissantes' : 'Battantes'}

Normes de fabrication APEX 4.0

  • Structure ${PANEL.structure} mm / Renforts ${PANEL.reinforcement} mm / Dos ${PANEL.back} mm
  • Chants ABS ${EDGE_BANDING.abs} mm visible — Dos: ABS sur ${EDGE_BANDING.backPanelSides} chants
  • Rainure dos ${GROOVE.back.width}×${GROOVE.back.depth} mm — position à ${GROOVE.back.axisFromPanelEdge} mm du bord
  • System 32 côtés : Ø${SYSTEM32.holeDiameter} mm borgne, prof. ${SYSTEM32.holeDepth} mm, pas ${SYSTEM32.pitch} mm, offset ${SYSTEM32.firstHoleOffset} mm
  • System 32 séparateurs : Ø${SYSTEM32_SEPARATORS.holeDiameter} mm traversant, offset ${SYSTEM32_SEPARATORS.firstHoleOffset} mm
  • Connecteurs ${CONNECTORS.ixconnect.model} (structure) : about Ø${CONNECTORS.ixconnect.edgeHole.diameter}×${CONNECTORS.ixconnect.edgeHole.depth} / face Ø${CONNECTORS.ixconnect.faceHole.diameter}×${CONNECTORS.ixconnect.faceHole.depth}
  • Connecteurs ${CONNECTORS.ixconnectFinish.model} (finitions) : face Ø${CONNECTORS.ixconnectFinish.faceHole.diameter}×${CONNECTORS.ixconnectFinish.faceHole.depth}, axes ${CONNECTORS.ixconnectFinish.axes.back}/${CONNECTORS.ixconnectFinish.axes.front} mm
  • Ixconnect: extrémités d’arête T@${CONNECTORS.jointPattern.tourillon1Axis} → IX@${CONNECTORS.jointPattern.ixconnectAxis} → T@${CONNECTORS.jointPattern.tourillon2Axis} mm (depuis bord avant) · jonctions internes dessus/fond: IX au milieu profondeur + T ±${CONNECTORS.jointPatternMiddleTourillonOffsetFromIxMm} mm — faces séparateurs verticaux sans Ixconnect
  • Renforts : 1× ${REINFORCEMENT_MACHINING.connectorModel} au centre + 2× tourillons Ø${REINFORCEMENT_MACHINING.tourillonDiameter} à ${REINFORCEMENT_MACHINING.tourillonEdgeOffset} mm des bords (axes selon largeur renfort)
  • Charnières ${HINGE.model} : axe à ${HINGE.axisOffset} mm du haut et du bas (réversible)
  • Matrice charnières: ≤900mm=2, 901-1600=3, 1601-2000=4, 2001-2400=5, 2401-2600=6
  • Étagères : ${SHELF_MACHINING.tourillonCount}× tourillons Ø${SHELF_MACHINING.tourillonDiameter} — ${SHELF_MACHINING.shelvesPerStratum} rayons/strate · Rayons mobiles SP 15/8/5 : face Ø20×15 (axe 9.5) + chant Ø8×37 (à 6 du dessous) · avant 32 / arrière 37
  • Plinthe : ${PLINTH.model} (pieds ${PLINTH.footHeight} mm + clips)
  • Dessus/Fond : ABS ${EDGE_BANDING.abs} mm avant ET arrière
  • ${config.hardware?.handleType === 'full_height' ? `
  • Poignée toute hauteur : déduction ${HANDLE.fullHeightWidthDeductionMm} mm en largeur porte ; rainure ${HANDLE.fullHeightGroove.width}×${HANDLE.fullHeightGroove.depth} mm côté opposé aux charnières ; retrait mécanique ${HANDLE.fullHeightTopBottomOffset} mm haut et bas
  • ` : ''}
Vue de face (Élévation)
${plans.assembly}
Vue de côté
${plans.side}
Vue de dessus
${plans.top}
Détail constructif — Côté
${plans.detail}
`; // Cutting list page if (cuttingList) { html += `

Liste de débitage

Résumé

Nombre de pièces:${cuttingList.summary.totalPieces}
Surface totale:${cuttingList.summary.totalSurfaceM2.toFixed(2)} m²
Chant ABS 2mm:${cuttingList.summary.edgeBandingMl} ml
Finitions:${cuttingList.summary.finishCount}
`; for (const group of cuttingList.byFinish) { html += `

Finition: ${group.finishLabel}

`; for (const piece of group.pieces) { const machStr = piece.machining ? Object.keys(piece.machining).join(', ') : '-'; html += ` `; } html += `
Description Largeur Hauteur Ép. Qté Surface Usinage
${piece.description} ${piece.widthMm} mm ${piece.heightMm} mm ${piece.thicknessMm} mm ${piece.quantity} ${piece.surfaceM2.toFixed(3)} m² ${machStr}
Sous-total ${group.pieceCount} pcs ${group.totalSurfaceM2.toFixed(2)} m²
`; } html += `
`; // Piece-by-piece machining plan pages for (const piece of (cuttingList.pieces || [])) { if (!piece.piecePlanSvg) continue; html += `

Plan pièce - ${piece.description}

Fini: ${piece.widthMm} x ${piece.heightMm} x ${piece.thicknessMm} mm  |  Brut CNC: ${piece.rawCutWidthMm} x ${piece.rawCutHeightMm} mm  |  Qté: ${piece.quantity}

${piece.piecePlanSvg}
`; } } // Packing list page (layer-based per APEX 4.0) if (packingList && packingList.boxes && packingList.boxes.length > 0) { html += `

Liste de colisage — Stratégie par niveaux (APEX 4.0)

Résumé

Nombre de colis:${packingList.totalBoxes}
Poids max/colis:${packingList.maxWeightKg} kg
Colis le plus lourd:${packingList.heaviestBoxKg.toFixed(1)} kg
Poids total:${packingList.totalWeightKg.toFixed(1)} kg
`; // Layer summary table if (packingList.layerSummary) { const ls = packingList.layerSummary; html += `
NiveauContenuColisPièces
1${ls.level1.label}${ls.level1.boxes}${ls.level1.items}
2${ls.level2.label}${ls.level2.boxes}${ls.level2.items}
3${ls.level3.label}${ls.level3.boxes}${ls.level3.items}
`; } for (const box of packingList.boxes) { html += `

Colis ${box.id}${box.level ? ` — ${box.level}` : ''} — ${box.totalWeightKg.toFixed(1)} kg

`; for (const item of box.pieces) { html += ` `; } html += `
PièceDim. (L×H×Ép.)QtéPoids
${item.description} ${item.widthMm}×${item.heightMm}×${item.thicknessMm} mm ${item.quantity} ${item.weightKg.toFixed(1)} kg
`; } html += `
`; } html += ` `; return html; } export default { generateAssemblyPlan, generateSidePlan, generateTopPlan, generateDetailPlan, generateAllPlans, generatePDFHtml };