/**
* 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
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}
Description
Largeur
Hauteur
Ép.
Qté
Surface
Usinage
`;
for (const piece of group.pieces) {
const machStr = piece.machining
? Object.keys(piece.machining).join(', ')
: '-';
html += `
${piece.description}
${piece.widthMm} mm
${piece.heightMm} mm
${piece.thicknessMm} mm
${piece.quantity}
${piece.surfaceM2.toFixed(3)} m²
${machStr}
`;
}
html += `
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 += `
Niveau Contenu Colis Piè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
Pièce Dim. (L×H×Ép.) Qté Poids
`;
for (const item of box.pieces) {
html += `
${item.description}
${item.widthMm}×${item.heightMm}×${item.thicknessMm} mm
${item.quantity}
${item.weightKg.toFixed(1)} kg
`;
}
html += `
`;
}
html += `
`;
}
html += `
`;
return html;
}
export default {
generateAssemblyPlan,
generateSidePlan,
generateTopPlan,
generateDetailPlan,
generateAllPlans,
generatePDFHtml
};