Co-authored-by: Cursor <[email protected]>
This commit is contained in:
lejay
2026-05-28 07:20:38 +02:00
co-authored by Cursor
parent 6923c319a1
commit 6c0d7e47b8
81 changed files with 21780 additions and 2986 deletions
+522
View File
@@ -0,0 +1,522 @@
/**
* Export MPR (WOODWOP / Homag-family .mpr) pour le débit APEX — un fichier par panneau, ZIP.
* Coordonnées XY = même repère que le G-code (.nc commentaires): origine bas-gauche, mm.
* Macros RSO/* : chemins exemple — à présence confirmée sur le poste/cellule.
*/
import JSZip from 'jszip';
import { generateCuttingList } from './cuttingEngine.js';
import { collectHoleSpecsForPiece } from './holeSpecifications.js';
import { GROOVE, getBackGroovePlanOrientation, BACK_GROOVE_PLAN_VERTICAL_TYPES } from '../../shared/manufacturing.js';
const MPR_ARTICLE_CODES = Object.freeze({
plintheBas: 'PL_BAS',
bandeauHaut: 'BH',
dessus: 'BO_U',
fond: 'BO_L',
cotes: 'SE',
cotesDoublage: 'CD',
fileur: 'FI',
habillageFinitionGauche: 'FR_L_G',
habillageFinitionDroite: 'FR_L_D',
habillageFinitionDessus: 'FR_H',
dos: 'DO',
renforts: 'RE',
portes: 'TU',
panneauxInterieurs: 'ZA_O',
rayonFixe: 'RF_X',
etageres: 'EB_M',
renfortsTiroir: 'LABO'
});
function fmtMm(n) {
const v = Number(n);
if (!Number.isFinite(v)) return '0.0';
const rounded = Math.round(v * 10) / 10;
return Number.isInteger(rounded) ? `${rounded}.0` : `${rounded}`;
}
function toSafe(value, fb) {
const n = Number(value);
return Number.isFinite(n) ? n : fb;
}
function escQuoted(s) {
return String(s ?? '').replace(/"/g, "'");
}
/** Scinde ligne « gauche+droit » qty 2 en deux fichiers SE_L / SE_R. */
export function flattenPiecesForMprExport(pieces = []) {
const out = [];
for (const piece of pieces) {
const qty = Math.max(1, Math.round(toSafe(piece.quantity, 1)));
const splitSides =
piece.elementType === 'cotes' &&
qty === 2 &&
piece.id === 'piece-cotes' &&
String(piece.description || '').includes('gauche et droit');
if (splitSides) {
out.push({ piece: { ...piece, quantity: 1 }, slug: 'SE_L', qtyIndex: 0 });
out.push({ piece: { ...piece, quantity: 1 }, slug: 'SE_R', qtyIndex: 1 });
continue;
}
let baseSlug = MPR_ARTICLE_CODES[piece.elementType] || String(piece.elementType || '').toUpperCase() || 'PIECE';
for (let qi = 0; qi < qty; qi++) {
let slug = baseSlug;
if (
qty > 1 &&
['panneauxInterieurs', 'renforts', 'etageres', 'rayonFixe', 'bandeauHaut'].includes(piece.elementType || '')
) {
slug = `${baseSlug}_${qi + 1}`;
}
out.push({ piece, slug, qtyIndex: qi });
}
}
return out;
}
function mprAnglesForPiece(piece) {
const et = piece.elementType || '';
if (habillageEtPorte(et)) {
return {
DRWIN: '270.0',
beschGedreht: '0',
turnMsg: et === 'portes' ? 'DREHEN X-ACHSE' : 'DREHEN X-ACHSE'
};
}
if (['dessus', 'fond'].includes(et)) {
return {
DRWIN: '0.0',
beschGedreht: '1',
turnMsg: 'DREHEN Y-ACHSE'
};
}
return {
DRWIN: '0.0',
beschGedreht: '0',
turnMsg: 'DREHEN Y-ACHSE'
};
}
function habillageEtPorte(et) {
return (
['habillageFinitionGauche', 'habillageFinitionDroite', 'habillageFinitionDessus', 'portes'].includes(
et
)
);
}
/** Kantendicken : habillages façon exemple FR_* (tous faces 1 mm). */
function kantenFlags(piece) {
const fin = habillageEtPorte(piece.elementType || '');
if (fin) {
return { kavo: '1.0', kali: '1.0', kahi: '1.0', kare: '1.0' };
}
return { kavo: '1.0', kali: '0.0', kahi: '0.0', kare: '0.0' };
}
function grooveSection(piece, _Lmm, _Bmm) {
const m = piece?.machining || {};
const orient = piece?.elementType ? getBackGroovePlanOrientation(piece.elementType) : null;
if (!m.groove || !orient) return [];
const gw = fmtMm(Number(m.groove.width ?? GROOVE.back.width));
const gd = fmtMm(Number(m.groove.depth ?? GROOVE.back.depth));
const abst = fmtMm(Number(m.groove.axisFromPanelEdge ?? m.groove.axisFromBackEdge ?? GROOVE.back.axisFromPanelEdge));
const vertical = BACK_GROOVE_PLAN_VERTICAL_TYPES.includes(piece.elementType);
const win = vertical ? '180.0' : '270.0';
return [
'<121 \\Block\\',
'MNM="Kantenbearbeitungen von oben"',
'DP="1"',
'KO="04"',
'<139 \\Komponente\\',
'IN="RSO/Makro/RSO_Nut_Flaeche.mpr"',
'MNM="Nut Flaeche rueckseite"',
'XA="L"',
'YA="B"',
'VA="LA L"',
`VA="BR ${gw}"`,
`VA="TI ${gd}"`,
`VA="ABST ${abst}"`,
`VA="WIN ${win}"`,
'VA="ANF 0.0"',
'VA="END 0.0"',
'VA="MAT MAT"',
'KO="04"'
];
}
/** Un trou → lignes BohrVert (projection face Z+ ; perçages chant restent approximation LS/LSL). */
function bohrVertLines(spec, thicknessMm) {
const dRaw = Number(spec.diameterMm) || 5;
const depthRaw = Number(spec.depthMm) || 12;
const edgeHint = Boolean(spec.toolMode === 'edge' || spec.face === 'end');
const throughHint = Boolean(spec.toolMode === 'through');
const looksThrough = depthRaw >= thicknessMm - 0.5;
let bm = 'LS';
let ti;
if (throughHint || looksThrough) {
bm = 'LSL';
ti = `${fmtMm(thicknessMm)} + 1`;
} else if (edgeHint && depthRaw >= 38) {
bm = 'LSL';
ti = fmtMm(depthRaw);
} else {
ti = fmtMm(depthRaw);
}
const name = fmtMm(Math.round(dRaw * 10) / 10);
const du = fmtMm(Math.round(dRaw * 10) / 10);
const xa = fmtMm(spec.xMm);
const ya = fmtMm(spec.yMm);
return [
'<102 \\BohrVert\\',
`MNM="Bohrung ${name} mm"`,
`XA="${xa}"`,
`YA="${ya}"`,
`BM="${bm}"`,
`TI="${ti}"`,
`DU="${du}"`,
'AN="1"',
'MI="0"',
'S_="2"',
'ASG="2"',
'MLM="0"',
'KO="04"'
];
}
/** Supprime les doublons numériques (tolérance 0.02 mm — sans fusionner trous légitimes proches). */
function dedupeHoleSpecs(specs, eps = 0.02) {
const out = [];
for (const s of specs) {
const hit = out.find(
(o) =>
Math.abs(o.xMm - s.xMm) < eps &&
Math.abs(o.yMm - s.yMm) < eps &&
Math.abs((o.diameterMm || 0) - (s.diameterMm || 0)) < eps
);
if (!hit) out.push(s);
}
return out;
}
function buildSingleMprText({
piece,
slug,
qtyIndex,
seqGlobal,
groupIndex,
orderReference,
stackListKm,
defaultMaterialArticle
}) {
const angles = mprAnglesForPiece(piece);
const kant = kantenFlags(piece);
const L = toSafe(piece.rawCutWidthMm, piece.widthMm);
const B = toSafe(piece.rawCutHeightMm, piece.heightMm);
const D = toSafe(piece.thicknessMm, 19);
const posLabel = `${groupIndex}.${seqGlobal}`;
const { specs, thicknessMm } = collectHoleSpecsForPiece(piece, L, B, qtyIndex);
const unique = dedupeHoleSpecs(specs);
/* Bloc Bohrungen */
const holeLines = [];
for (const sp of unique) {
holeLines.push(...bohrVertLines(sp, thicknessMm));
}
const dp = Math.max(1, Math.min(98, holeLines.filter((ln) => ln.startsWith('<102')).length));
const matKm = escQuoted(
piece.finishId != null && piece.finishId !== '' ? piece.finishId : defaultMaterialArticle || 'APEX'
);
const lines = [];
lines.push('[H', 'VERSION="4.0 Alpha"', 'FW="1400"', 'FM="1"', 'ZS="40"', 'OP="1"');
lines.push('[001', 'MAT="2"', 'KM="Materialart"', `DRWIN="${angles.DRWIN}"`, 'KM="Drehwinkel 0,90,180,270"');
lines.push(`AUFT="${escQuoted(orderReference)}"`, `KM="AUFTRAG: ${escQuoted(orderReference)}"`);
lines.push(`STKL="${escQuoted(stackListKm)}"`, `KM="STUECKLISTE: ${escQuoted(stackListKm)}"`);
lines.push(`POS="${escQuoted(posLabel)}"`, `KM="POSITION: ${escQuoted(posLabel)}"`);
lines.push('BEZ="0"', `KM="Bezeichnung: ${escQuoted(slug)}"`);
lines.push(`L="${fmtMm(L)}"`, 'KM="Laenge in X"', `B="${fmtMm(B)}"`, 'KM="Breite in Y"', `D="${fmtMm(D)}"`, 'KM="Dicke in Z"');
lines.push('ZF="2.0"', 'KM="Zugabe Fraesung"', 'ZB="3.0"', 'KM="Zugabe Bohrung"');
lines.push('RITZEN="1.0"', 'KM="Vorritztiefe mm"');
lines.push(`KAVODI="${kant.kavo}"`, 'KM="Kantendicke vorne"', `KALIDI="${kant.kali}"`, 'KM="Kantendicke links"');
lines.push(`KAHIDI="${kant.kahi}"`, 'KM="Kantendicke hinten"', `KAREDI="${kant.kare}"`, 'KM="Kantendicke rechts"');
lines.push('FORMAT="0"', 'KM="Formatieren 0 oder 1"');
lines.push('ZUGLI="0.0"', 'ZUGRE="0.0"', 'ZUGVO="0.0"', 'ZUGHI="0.0"');
lines.push('DW="IF DRWIN=0 OR DRWIN=90 OR DRWIN=180 THEN DRWIN ELSE 270"');
lines.push('ld="IF DW=90 OR DW=270 THEN B ELSE L"', 'bd="IF DW=90 OR DW=270 THEN L ELSE B"');
lines.push(
'offxL="IF DW=180 THEN ZUGRE ELSE ZUGLI"',
'offxB="IF DW=90 THEN ZUGHI ELSE ZUGVO"',
'offyL="IF DW=180 THEN ZUGHI ELSE ZUGVO"',
'offyB="IF DW=90 THEN ZUGLI ELSE ZUGRE"'
);
lines.push(
'[K',
'<00 \\Koordinatensystem\\',
'NR="04"',
'XP="IF DW=0 OR DW=270 THEN 0 ELSE ld"',
'YP="IF DW=0 OR DW=90 THEN 0 ELSE bd"',
'ZP="0"',
'D1="DW"',
'KI="0"',
'D2="0"',
'MI="0"',
'<00 \\Koordinatensystem\\',
'NR="F1"',
'XP="IF DW=0 OR DW=270 THEN 0 ELSE ld"',
'YP="IF DW=0 OR DW=90 THEN 0 ELSE bd"',
'ZP="0"',
'D1="DW"',
'KI="90"',
'D2="0"',
'MI="0"',
'<00 \\Koordinatensystem\\',
'NR="F2"',
'XP="IF DW=0 OR DW=270 THEN 0 ELSE ld"',
'YP="IF DW=90 OR DW=0 THEN 0 ELSE bd"',
'ZP="0"',
'D1="DW+270"',
'KI="90"',
'D2="0"',
'MI="1"',
'<00 \\Koordinatensystem\\',
'NR="F3"',
'XP="IF DW=0 OR DW=90 THEN 0 ELSE ld"',
'YP="IF DW=180 OR DW=90 THEN 0 ELSE bd"',
'ZP="0"',
'D1="DW+180"',
'KI="90"',
'D2="0"',
'MI="1"',
'<00 \\Koordinatensystem\\',
'NR="F4"',
'XP="IF DW=180 OR DW=270 THEN 0 ELSE ld"',
'YP="IF DW=0 OR DW=270 THEN 0 ELSE bd"',
'ZP="0"',
'D1="DW+90"',
'KI="90"',
'D2="0"',
'MI="0"',
'<100 \\WerkStck\\',
'LA="IF DW=90 OR DW=270 THEN B ELSE L"',
'BR="IF DW=90 OR DW=270 THEN L ELSE B"',
'DI="D"',
'FNX="IF DW=90 OR DW=270 THEN offxB ELSE offxL"',
'FNY="IF DW=90 OR DW=270 THEN offyB ELSE offyL"',
'AX="IF DW=90 OR DW=270 THEN ZUGVO+ZUGHI ELSE ZUGLI+ZUGRE"',
'AY="IF DW=90 OR DW=270 THEN ZUGLI+ZUGRE ELSE ZUGVO+ZUGHI"',
'<101 \\Kommentar\\',
`KM="AUFTRAG : ${escQuoted(orderReference)}"`,
`KM="STUECKLISTE : ${escQuoted(stackListKm)}"`,
`KM="POSITION : ${escQuoted(posLabel)}"`,
`KM="Bezeichnung: ${escQuoted(slug)}"`,
'KM=""',
`KM="${escQuoted(angles.turnMsg)}"`,
'KM=""',
'KM="Teileinfo"',
`KM="Groesse: ${fmtMm(L)} x ${fmtMm(B)}"`,
`KM="Staerke: ${fmtMm(D)}"`,
`KM=""`,
`KM="Material: ${matKm}"`,
'KM="Belag IN: "',
'KM="Belag AU: "',
'KM=""',
'<139 \\Komponente\\',
'MNM="Beschriftung"',
'IN="RSO/Makro/RSO_Beschriftung.mpr"',
'VA="L L"',
'VA="B B"',
'VA="KAVODI KAVODI"',
'VA="KALIDI KALIDI"',
'VA="KAHIDI KAHIDI"',
'VA="KAREDI KAREDI"',
`VA="GEDREHT ${angles.beschGedreht}"`,
'VA="NCSTOPP 0"',
'KO="04"',
'<139 \\Komponente\\',
'MNM="Formatierung"',
'IN="RSO/Makro/RSO_Format.mpr"',
'VA="L L"',
'VA="B B"',
'VA="D D"',
'VA="WKZ 316"',
'VA="KAVODI KAVODI"',
'VA="KALIDI KALIDI"',
'VA="KAHIDI KAHIDI"',
'VA="KAREDI KAREDI"',
'VA="DIST 0.0"',
'VA="ECKRUND 0.1"',
'VA="KAFR 0"',
'VA="KAFRMAX 2.0"',
'VA="MAT MAT"',
'??="FORMAT"',
'KO="04"',
'<121 \\Block\\',
'MNM="Bearbeitungen von oben "',
`DP="${dp}"`,
'KO="04"'
);
lines.push(...holeLines);
lines.push(...grooveSection(piece, L, B));
lines.push('!');
return lines.join('\r\n');
}
function attachGlobalSeq(pieceFlat, allFlat) {
return pieceFlat.map((entry) => {
const globalIdx = allFlat.findIndex(
(f) =>
f.piece.id === entry.piece.id &&
f.qtyIndex === entry.qtyIndex &&
f.slug === entry.slug
);
return { ...entry, seqGlobal: globalIdx >= 0 ? globalIdx + 1 : 1 };
});
}
function buildMprFileRecords(entries, mprOptions) {
const { groupIndex = 1, orderReference = '', stackListKm = '01', defaultMaterialArticle = '' } = mprOptions;
return entries.map((entry) => {
const fileName = `${groupIndex}_${entry.seqGlobal}_${safeZipFileName(entry.slug)}.mpr`;
const content = buildSingleMprText({
piece: entry.piece,
slug: entry.slug,
qtyIndex: entry.qtyIndex,
seqGlobal: entry.seqGlobal,
groupIndex,
orderReference,
stackListKm,
defaultMaterialArticle
});
return { fileName, content };
});
}
async function packMprRecords(records, zipBaseName) {
if (records.length === 1) {
const only = records[0];
return {
buffer: Buffer.from(only.content, 'utf8'),
fileName: only.fileName,
mimeType: 'application/octet-stream',
fileCount: 1
};
}
const zip = new JSZip();
for (const rec of records) {
zip.file(rec.fileName, rec.content);
}
const safeBase = safeZipFileName(zipBaseName || 'piece-mpr');
return {
buffer: await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }),
fileName: `${safeBase}.zip`,
mimeType: 'application/zip',
fileCount: records.length
};
}
/** Nombre de fichiers MPR générés pour une ligne de débitage (ex. côtés L+R → 2). */
export function countMprInstancesForPiece(piece) {
return flattenPiecesForMprExport([piece]).length;
}
export async function generateMprZipBuffer({
config,
modules = [],
colors = {},
orderReference = '',
stackListKm = '01',
defaultMaterialArticle = '',
/** Index groupe commande dans le nom fichier (premier nombre). */
groupIndex = 1
}) {
const cuttingList = generateCuttingList(config, colors, modules);
const flat = flattenPiecesForMprExport(cuttingList.pieces);
const withSeq = attachGlobalSeq(flat, flat);
const records = buildMprFileRecords(withSeq, {
groupIndex,
orderReference,
stackListKm,
defaultMaterialArticle
});
return {
...(await packMprRecords(records, `apex-mpr-g${groupIndex}`)),
fileCount: records.length
};
}
/**
* Export MPR pour une seule ligne de la liste de débitage (identifiée par pieceId).
* @param {number|null|undefined} instanceIndex — exemplaire 0-based ; omis = tous les exemplaires de la ligne.
*/
export async function generateMprPieceDownload({
config,
modules = [],
colors = {},
pieceId,
instanceIndex,
orderReference = '',
stackListKm = '01',
defaultMaterialArticle = '',
groupIndex = 1
}) {
if (!pieceId || typeof pieceId !== 'string') {
throw new Error('pieceId requis.');
}
const cuttingList = generateCuttingList(config, colors, modules);
const piece = (cuttingList.pieces || []).find((p) => p.id === pieceId);
if (!piece) {
throw new Error('Pièce introuvable dans la liste de débitage.');
}
const allFlat = flattenPiecesForMprExport(cuttingList.pieces);
let pieceFlat = attachGlobalSeq(flattenPiecesForMprExport([piece]), allFlat);
if (instanceIndex != null && instanceIndex !== '') {
const idx = Math.floor(Number(instanceIndex));
if (!Number.isFinite(idx) || idx < 0) {
throw new Error('instanceIndex invalide.');
}
pieceFlat = pieceFlat.filter((entry) => entry.qtyIndex === idx);
if (pieceFlat.length === 0) {
throw new Error('Exemplaire introuvable pour cette pièce.');
}
}
const records = buildMprFileRecords(pieceFlat, {
groupIndex,
orderReference,
stackListKm,
defaultMaterialArticle
});
return packMprRecords(records, pieceId);
}
function safeZipFileName(seg) {
return String(seg).replace(/[^\w\-]+/g, '_').replace(/_+/g, '_');
}
export default {
generateMprZipBuffer,
generateMprPieceDownload,
countMprInstancesForPiece,
flattenPiecesForMprExport
};