+583
-183
@@ -1,170 +1,113 @@
|
||||
/**
|
||||
* Plan Generator - Generate 2D assembly and parts plans in SVG format
|
||||
* Also handles PDF export
|
||||
* 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 ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate SVG for assembly view (front elevation)
|
||||
* Draw System 32 hole column (vertical series of holes)
|
||||
*/
|
||||
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" />`;
|
||||
}
|
||||
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 += `<circle cx="${x}" cy="${topY + yOffset}" r="${r}" class="s32-hole" />`;
|
||||
}
|
||||
|
||||
// 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" />`;
|
||||
}
|
||||
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 += `<rect x="${hingeLineX - markerW / 2}" y="${y - markerH / 2}" width="${markerW}" height="${markerH}" class="hinge-marker" />`;
|
||||
}
|
||||
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 = `<line x1="${axisX}" y1="${y1}" x2="${axisX}" y2="${y2}" class="fh-groove" />`;
|
||||
const tx = axisX + (hingeSide === 'right' ? 5 : -5);
|
||||
const anchor = hingeSide === 'right' ? 'start' : 'end';
|
||||
svg += `<text x="${tx}" y="${doorY + doorHeightMm * scale * 0.45}" class="detail-label" text-anchor="${anchor}" fill="#0d9488">Rainure poignée TH (${gw} mm) · ${off} mm H/B</text>`;
|
||||
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 += `<line x1="${x + grooveOffset}" y1="${y}" x2="${x + grooveOffset}" y2="${y + length}" class="groove" />`;
|
||||
} 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;
|
||||
}
|
||||
svg += `<line x1="${x}" y1="${y + grooveOffset}" x2="${x + length}" y2="${y + grooveOffset}" class="groove" />`;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -173,25 +116,366 @@ export function generateTopPlan(config, modules = []) {
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
|
||||
svg += `<style>${SHARED_STYLES}</style>`;
|
||||
|
||||
// Title
|
||||
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de face - Élévation</text>`;
|
||||
|
||||
const oX = MARGIN;
|
||||
const oY = MARGIN;
|
||||
|
||||
// Plinth
|
||||
const plinthY = oY + bodyHeight * S;
|
||||
svg += `<rect x="${oX}" y="${plinthY}" width="${widthMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
|
||||
// Body
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${widthMm * S}" height="${bodyHeight * S}" class="body" />`;
|
||||
|
||||
// Module separators
|
||||
if (modules.length > 0) {
|
||||
let currentX = oX;
|
||||
for (let i = 0; i < modules.length - 1; i++) {
|
||||
currentX += modules[i].widthMm * S;
|
||||
svg += `<line x1="${currentX}" y1="${oY}" x2="${currentX}" y2="${plinthY}" class="separator" />`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 += `<rect x="${oX + i * panelWidth * 0.95 + 5}" y="${oY + 5}" width="${panelWidth * 0.9}" height="${(bodyHeight - 10) * S}" class="door" />`;
|
||||
}
|
||||
} 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 += `<rect x="${doorX}" y="${doorY}" width="${doorWPx}" height="${doorHmm * S}" class="door" />`;
|
||||
|
||||
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 += `<rect x="${renfortX}" y="${y}" width="${renfortW}" height="${renfortH}" class="renfort" />`;
|
||||
}
|
||||
svg += `<text x="${oX + (widthMm * S) / 2}" y="${svgHeight - 10}" class="detail-label" text-anchor="middle" fill="#6b5344" font-size="7.5">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 »</text>`;
|
||||
|
||||
// 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 += '</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
|
||||
svg += `<style>${SHARED_STYLES}</style>`;
|
||||
|
||||
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de côté</text>`;
|
||||
|
||||
const oX = MARGIN;
|
||||
const oY = MARGIN;
|
||||
|
||||
// Plinth
|
||||
const plinthY = oY + bodyHeight * S;
|
||||
svg += `<rect x="${oX}" y="${plinthY}" width="${depthMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
|
||||
// Body
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${depthMm * S}" height="${bodyHeight * S}" class="body" />`;
|
||||
|
||||
// Back panel (8mm) shown as a thin rectangle at the back
|
||||
const backX = oX + (depthMm - PANEL.back) * S;
|
||||
svg += `<rect x="${backX}" y="${oY + PANEL.structure * S}" width="${PANEL.back * S}" height="${(bodyHeight - PANEL.structure * 2) * S}" fill="#d4c4a4" stroke="#666" stroke-width="0.5" />`;
|
||||
svg += `<text x="${backX - 2}" y="${oY + bodyHeight * S / 2}" class="detail-label" text-anchor="end">Dos ${PANEL.back}mm</text>`;
|
||||
|
||||
// Groove indicator (dashed green line at 16mm from panel edge)
|
||||
const grooveAxisX = oX + (depthMm - GROOVE.back.axisFromPanelEdge) * S;
|
||||
svg += `<line x1="${grooveAxisX}" y1="${oY + PANEL.structure * S}" x2="${grooveAxisX}" y2="${plinthY - PANEL.structure * S}" class="groove" />`;
|
||||
svg += `<text x="${grooveAxisX - 3}" y="${oY + bodyHeight * S * 0.3}" class="detail-label" text-anchor="end" fill="#2a9d8f">Rainure ${GROOVE.back.width}×${GROOVE.back.depth} (16mm du bord)</text>`;
|
||||
|
||||
// System 32 holes along the side panel
|
||||
svg += drawSystem32Holes(oX + 37 * S, oY + PANEL.structure * S, bodyHeight - PANEL.structure * 2, S);
|
||||
svg += `<text x="${oX + 37 * S}" y="${oY - 5}" class="detail-label" text-anchor="middle" fill="#e63946">System 32</text>`;
|
||||
|
||||
// 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 += '</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
|
||||
svg += `<style>${SHARED_STYLES}</style>`;
|
||||
|
||||
svg += `<text x="${svgWidth / 2}" y="25" class="title" text-anchor="middle">Vue de dessus</text>`;
|
||||
|
||||
const oX = MARGIN;
|
||||
const oY = MARGIN;
|
||||
|
||||
// Body outline
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${widthMm * S}" height="${depthMm * S}" class="body" />`;
|
||||
|
||||
// Back panel groove line (horizontal dashed line near panel edge)
|
||||
const grooveY = oY + (depthMm - PANEL.structure) * S;
|
||||
svg += `<line x1="${oX + PANEL.structure * S}" y1="${grooveY}" x2="${oX + (widthMm - PANEL.structure) * S}" y2="${grooveY}" class="groove" />`;
|
||||
svg += `<text x="${oX + widthMm * S + 5}" y="${grooveY + 3}" class="detail-label" fill="#2a9d8f">Rainure dos</text>`;
|
||||
|
||||
// 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 += `<text x="${currentX + moduleWidth / 2}" y="${oY + depthMm * S / 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="${oY}" x2="${currentX}" y2="${oY + depthMm * S}" class="separator" />`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">`;
|
||||
svg += `<style>${SHARED_STYLES}
|
||||
.edge-band { fill: #ff9f1c; stroke: none; opacity: 0.6; }
|
||||
.callout { font-family: Arial, sans-serif; font-size: 8px; fill: #333; }
|
||||
.callout-line { stroke: #999; stroke-width: 0.3; fill: none; }
|
||||
.shelf-outline { fill: #f9fafb; stroke: #111827; stroke-width: 0.8; }
|
||||
.shelf-face-hole { fill: none; stroke: #ea580c; stroke-width: 0.55; }
|
||||
.shelf-edge-hole { fill: none; stroke: #7c3aed; stroke-width: 0.55; stroke-dasharray: 1.2 1.2; }
|
||||
</style>`;
|
||||
|
||||
svg += `<text x="${svgWidth / 2}" y="20" class="title" text-anchor="middle">Détail — Côté (vue intérieure)</text>`;
|
||||
|
||||
const oX = MARGIN + 40;
|
||||
const oY = MARGIN + 20;
|
||||
|
||||
// Panel outline
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${detailWidthMm * DS}" height="${detailHeightMm * DS}" class="body" stroke-width="2" />`;
|
||||
|
||||
// Edge banding (2mm ABS on all 4 edges)
|
||||
const eb = EDGE_BANDING.abs * DS;
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${detailWidthMm * DS}" height="${eb}" class="edge-band" />`; // top
|
||||
svg += `<rect x="${oX}" y="${oY + detailHeightMm * DS - eb}" width="${detailWidthMm * DS}" height="${eb}" class="edge-band" />`; // bottom
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${eb}" height="${detailHeightMm * DS}" class="edge-band" />`; // left
|
||||
svg += `<rect x="${oX + detailWidthMm * DS - eb}" y="${oY}" width="${eb}" height="${detailHeightMm * DS}" class="edge-band" />`; // 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 += `<rect x="${grooveX}" y="${oY + 5}" width="${grooveW}" height="${detailHeightMm * DS - 10}" fill="#2a9d8f" opacity="0.3" stroke="#2a9d8f" stroke-width="0.5" />`;
|
||||
// Callout
|
||||
svg += `<line x1="${grooveX + grooveW}" y1="${oY + 30}" x2="${grooveX + grooveW + 25}" y2="${oY + 20}" class="callout-line" />`;
|
||||
svg += `<text x="${grooveX + grooveW + 27}" y="${oY + 22}" class="callout">Rainure ${GROOVE.back.width}×${GROOVE.back.depth} mm</text>`;
|
||||
svg += `<text x="${grooveX + grooveW + 27}" y="${oY + 31}" class="callout">Position à ${GROOVE.back.axisFromPanelEdge} mm du bord</text>`;
|
||||
|
||||
// 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 += `<circle cx="${s32X}" cy="${holeY}" r="${r}" class="s32-hole" />`;
|
||||
}
|
||||
// Callout for System 32
|
||||
svg += `<line x1="${s32X}" y1="${oY + SYSTEM32.firstHoleOffset * DS}" x2="${oX - 15}" y2="${oY + SYSTEM32.firstHoleOffset * DS - 10}" class="callout-line" />`;
|
||||
svg += `<text x="${oX - 17}" y="${oY + SYSTEM32.firstHoleOffset * DS - 12}" class="callout" text-anchor="end">System 32 (borgne)</text>`;
|
||||
svg += `<text x="${oX - 17}" y="${oY + SYSTEM32.firstHoleOffset * DS - 3}" class="callout" text-anchor="end">Ø${SYSTEM32.holeDiameter} / prof. ${SYSTEM32.holeDepth} / pas ${SYSTEM32.pitch}</text>`;
|
||||
|
||||
// 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 += `<circle cx="${jT1X}" cy="${oY}" r="${tourR}" class="connector" opacity="0.5" />`; // tourillon 1
|
||||
svg += `<circle cx="${jIXX}" cy="${oY}" r="${ixEdgeR}" class="connector" />`; // Ixconnect
|
||||
svg += `<circle cx="${jT2X}" cy="${oY}" r="${tourR}" class="connector" opacity="0.5" />`; // tourillon 2
|
||||
// Face holes (Ixconnect only — Ø8×12mm)
|
||||
svg += `<circle cx="${jIXX}" cy="${oY + conn.faceHole.depth * DS}" r="${(conn.faceHole.diameter / 2) * DS}" class="connector" />`;
|
||||
// Callout
|
||||
svg += `<line x1="${jT2X + 5}" y1="${oY}" x2="${jT2X + 30}" y2="${oY - 15}" class="callout-line" />`;
|
||||
svg += `<text x="${jT2X + 32}" y="${oY - 22}" class="callout">Ixconnect ${conn.model}</text>`;
|
||||
svg += `<text x="${jT2X + 32}" y="${oY - 13}" class="callout">About: Ø${conn.edgeHole.diameter}×${conn.edgeHole.depth} / Face: Ø${conn.faceHole.diameter}×${conn.faceHole.depth}</text>`;
|
||||
svg += `<text x="${jT2X + 32}" y="${oY - 4}" class="callout">Pattern: T@${pattern.tourillon1Axis} → IX@${pattern.ixconnectAxis} → T@${pattern.tourillon2Axis}</text>`;
|
||||
|
||||
// Panel thickness label
|
||||
svg += `<text x="${oX + detailWidthMm * DS / 2}" y="${oY + detailHeightMm * DS + 15}" class="callout" text-anchor="middle">Épaisseur: ${PANEL.structure} mm — Chant ABS ${EDGE_BANDING.abs} mm</text>`;
|
||||
|
||||
// --- 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 += `<text x="${insetX + insetW / 2}" y="${insetY - 16}" class="callout" text-anchor="middle">Rayons mobiles — SP 15/8/5 (dessous)</text>`;
|
||||
svg += `<rect x="${insetX}" y="${insetY}" width="${insetW}" height="${insetH}" class="shelf-outline" />`;
|
||||
|
||||
// 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 += `<circle cx="${cx}" cy="${cy}" r="${rFace}" class="shelf-face-hole" />`;
|
||||
}
|
||||
// Edge holes (against side panels) — left/right edge
|
||||
svg += `<circle cx="${insetX}" cy="${cy}" r="${rEdge}" class="shelf-edge-hole" />`;
|
||||
svg += `<circle cx="${insetX + insetW}" cy="${cy}" r="${rEdge}" class="shelf-edge-hole" />`;
|
||||
}
|
||||
|
||||
// Callouts
|
||||
svg += `<line x1="${insetX + insetW}" y1="${insetY + 10}" x2="${insetX + insetW + 22}" y2="${insetY}" class="callout-line" />`;
|
||||
svg += `<text x="${insetX + insetW + 24}" y="${insetY + 2}" class="callout">Face: Ø20×15 — axe 9.5 mm</text>`;
|
||||
svg += `<text x="${insetX + insetW + 24}" y="${insetY + 12}" class="callout">Chant: Ø8×37 — à 6 mm du dessous</text>`;
|
||||
svg += `<text x="${insetX + insetW + 24}" y="${insetY + 22}" class="callout">Avant: 32 mm · Arrière: 37 mm</text>`;
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
}
|
||||
|
||||
@@ -203,8 +487,10 @@ export function generateAllPlans(config, modules = [], colors = {}) {
|
||||
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,
|
||||
@@ -220,14 +506,14 @@ export function generateAllPlans(config, modules = [], colors = {}) {
|
||||
/**
|
||||
* Generate HTML page with all plans for PDF export
|
||||
*/
|
||||
export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
export function generatePDFHtml(config, modules = [], cuttingList = null, packingList = null) {
|
||||
const plans = generateAllPlans(config, modules);
|
||||
|
||||
|
||||
let html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Plans Armoire</title>
|
||||
<title>Plans Armoire — APEX 4.0</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 15mm; }
|
||||
body { font-family: Arial, sans-serif; font-size: 11px; }
|
||||
@@ -244,15 +530,19 @@ export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
.summary h3 { margin: 0 0 10px; }
|
||||
.specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.spec-item { display: flex; justify-content: space-between; }
|
||||
.mfg-specs { background: #fff8e7; padding: 12px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #c9a96e; }
|
||||
.mfg-specs h4 { margin: 0 0 8px; color: #8b7355; }
|
||||
.mfg-specs ul { margin: 0; padding-left: 18px; }
|
||||
.mfg-specs li { margin: 2px 0; }
|
||||
</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>
|
||||
<p class="subtitle">APEX 4.0 — 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">
|
||||
@@ -264,25 +554,53 @@ export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
<div class="spec-item"><span>Portes:</span><strong>${config.doorType === 'sliding' ? 'Coulissantes' : 'Battantes'}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mfg-specs">
|
||||
<h4>Normes de fabrication APEX 4.0</h4>
|
||||
<ul>
|
||||
<li>Structure ${PANEL.structure} mm / Renforts ${PANEL.reinforcement} mm / Dos ${PANEL.back} mm</li>
|
||||
<li>Chants ABS ${EDGE_BANDING.abs} mm visible — Dos: ABS sur ${EDGE_BANDING.backPanelSides} chants</li>
|
||||
<li>Rainure dos ${GROOVE.back.width}×${GROOVE.back.depth} mm — position à ${GROOVE.back.axisFromPanelEdge} mm du bord</li>
|
||||
<li>System 32 côtés : Ø${SYSTEM32.holeDiameter} mm borgne, prof. ${SYSTEM32.holeDepth} mm, pas ${SYSTEM32.pitch} mm, offset ${SYSTEM32.firstHoleOffset} mm</li>
|
||||
<li>System 32 séparateurs : Ø${SYSTEM32_SEPARATORS.holeDiameter} mm traversant, offset ${SYSTEM32_SEPARATORS.firstHoleOffset} mm</li>
|
||||
<li>Connecteurs ${CONNECTORS.ixconnect.model} (structure) : about Ø${CONNECTORS.ixconnect.edgeHole.diameter}×${CONNECTORS.ixconnect.edgeHole.depth} / face Ø${CONNECTORS.ixconnect.faceHole.diameter}×${CONNECTORS.ixconnect.faceHole.depth}</li>
|
||||
<li>Connecteurs ${CONNECTORS.ixconnectFinish.model} (finitions) : face Ø${CONNECTORS.ixconnectFinish.faceHole.diameter}×${CONNECTORS.ixconnectFinish.faceHole.depth}, axes ${CONNECTORS.ixconnectFinish.axes.back}/${CONNECTORS.ixconnectFinish.axes.front} mm</li>
|
||||
<li>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</li>
|
||||
<li>Renforts : 1× ${REINFORCEMENT_MACHINING.connectorModel} au centre + 2× tourillons Ø${REINFORCEMENT_MACHINING.tourillonDiameter} à ${REINFORCEMENT_MACHINING.tourillonEdgeOffset} mm des bords (axes selon largeur renfort)</li>
|
||||
<li>Charnières ${HINGE.model} : axe à ${HINGE.axisOffset} mm du haut et du bas (réversible)</li>
|
||||
<li>Matrice charnières: ≤900mm=2, 901-1600=3, 1601-2000=4, 2001-2400=5, 2401-2600=6</li>
|
||||
<li>É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</li>
|
||||
<li>Plinthe : ${PLINTH.model} (pieds ${PLINTH.footHeight} mm + clips)</li>
|
||||
<li>Dessus/Fond : ABS ${EDGE_BANDING.abs} mm avant ET arrière</li>
|
||||
${config.hardware?.handleType === 'full_height' ? `<li>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</li>` : ''}
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
<div class="page">
|
||||
<div class="plan-container">
|
||||
<div class="plan-title">Détail constructif — Côté</div>
|
||||
${plans.detail}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
// Cutting list page
|
||||
if (cuttingList) {
|
||||
html += `
|
||||
@@ -293,11 +611,11 @@ export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
<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>Chant ABS 2mm:</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>
|
||||
@@ -310,11 +628,15 @@ export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
<th>Ép.</th>
|
||||
<th>Qté</th>
|
||||
<th>Surface</th>
|
||||
<th>Usinage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
|
||||
for (const piece of group.pieces) {
|
||||
const machStr = piece.machining
|
||||
? Object.keys(piece.machining).join(', ')
|
||||
: '-';
|
||||
html += `
|
||||
<tr>
|
||||
<td>${piece.description}</td>
|
||||
@@ -323,36 +645,114 @@ export function generatePDFHtml(config, modules = [], cuttingList = null) {
|
||||
<td>${piece.thicknessMm} mm</td>
|
||||
<td>${piece.quantity}</td>
|
||||
<td>${piece.surfaceM2.toFixed(3)} m²</td>
|
||||
<td>${machStr}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4">Sous-total</th>
|
||||
<th>${group.pieceCount}</th>
|
||||
<th colspan="5">Sous-total</th>
|
||||
<th>${group.pieceCount} pcs</th>
|
||||
<th>${group.totalSurfaceM2.toFixed(2)} m²</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
|
||||
html += `
|
||||
</div>`;
|
||||
|
||||
// Piece-by-piece machining plan pages
|
||||
for (const piece of (cuttingList.pieces || [])) {
|
||||
if (!piece.piecePlanSvg) continue;
|
||||
html += `
|
||||
<div class="page">
|
||||
<h2>Plan pièce - ${piece.description}</h2>
|
||||
<p style="margin:0 0 8px;color:#555;">
|
||||
Fini: ${piece.widthMm} x ${piece.heightMm} x ${piece.thicknessMm} mm
|
||||
|
|
||||
Brut CNC: ${piece.rawCutWidthMm} x ${piece.rawCutHeightMm} mm
|
||||
|
|
||||
Qté: ${piece.quantity}
|
||||
</p>
|
||||
<div class="plan-container">
|
||||
${piece.piecePlanSvg}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Packing list page (layer-based per APEX 4.0)
|
||||
if (packingList && packingList.boxes && packingList.boxes.length > 0) {
|
||||
html += `
|
||||
<div class="page">
|
||||
<h2>Liste de colisage — Stratégie par niveaux (APEX 4.0)</h2>
|
||||
<div class="summary">
|
||||
<h3>Résumé</h3>
|
||||
<div class="specs">
|
||||
<div class="spec-item"><span>Nombre de colis:</span><strong>${packingList.totalBoxes}</strong></div>
|
||||
<div class="spec-item"><span>Poids max/colis:</span><strong>${packingList.maxWeightKg} kg</strong></div>
|
||||
<div class="spec-item"><span>Colis le plus lourd:</span><strong>${packingList.heaviestBoxKg.toFixed(1)} kg</strong></div>
|
||||
<div class="spec-item"><span>Poids total:</span><strong>${packingList.totalWeightKg.toFixed(1)} kg</strong></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Layer summary table
|
||||
if (packingList.layerSummary) {
|
||||
const ls = packingList.layerSummary;
|
||||
html += `
|
||||
<table>
|
||||
<thead><tr><th>Niveau</th><th>Contenu</th><th>Colis</th><th>Pièces</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>${ls.level1.label}</td><td>${ls.level1.boxes}</td><td>${ls.level1.items}</td></tr>
|
||||
<tr><td>2</td><td>${ls.level2.label}</td><td>${ls.level2.boxes}</td><td>${ls.level2.items}</td></tr>
|
||||
<tr><td>3</td><td>${ls.level3.label}</td><td>${ls.level3.boxes}</td><td>${ls.level3.items}</td></tr>
|
||||
</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
for (const box of packingList.boxes) {
|
||||
html += `
|
||||
<h3>Colis ${box.id}${box.level ? ` — ${box.level}` : ''} — ${box.totalWeightKg.toFixed(1)} kg</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Pièce</th><th>Dim. (L×H×Ép.)</th><th>Qté</th><th>Poids</th></tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
for (const item of box.pieces) {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${item.description}</td>
|
||||
<td>${item.widthMm}×${item.heightMm}×${item.thicknessMm} mm</td>
|
||||
<td>${item.quantity}</td>
|
||||
<td>${item.weightKg.toFixed(1)} kg</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
html += `
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export default {
|
||||
generateAssemblyPlan,
|
||||
generateSidePlan,
|
||||
generateTopPlan,
|
||||
export default {
|
||||
generateAssemblyPlan,
|
||||
generateSidePlan,
|
||||
generateTopPlan,
|
||||
generateDetailPlan,
|
||||
generateAllPlans,
|
||||
generatePDFHtml
|
||||
generatePDFHtml
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user