Ship configurator updates with catalog DB included for server deploy. Co-authored-by: Cursor <[email protected]>
810 lines
26 KiB
JavaScript
810 lines
26 KiB
JavaScript
/**
|
|
* 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',
|
|
plintheGauche: 'PL_G',
|
|
plintheDroite: 'PL_D',
|
|
bandeauHaut: 'BH',
|
|
dessus: 'BO_U',
|
|
fond: 'BO_L',
|
|
cotes: 'SE',
|
|
cotesDoublage: 'CD',
|
|
fileur: 'FI',
|
|
fileurCotes: 'FI_C',
|
|
fileurHaut: 'FI_H',
|
|
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',
|
|
tiroirFond: 'TI_BO',
|
|
tiroirDos: 'TI_RW',
|
|
tiroirFacade: 'TI_FR'
|
|
});
|
|
|
|
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, "'");
|
|
}
|
|
|
|
/** Désignation « type » pour le bloc [001 (sans suffixe côté/index) — ex. SE_L → SE, ZA_O_2 → ZA. */
|
|
function baseDesignation(slug) {
|
|
const s = String(slug || '').trim();
|
|
const cut = s.indexOf('_');
|
|
return cut > 0 ? s.slice(0, cut) : s;
|
|
}
|
|
|
|
/** 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';
|
|
if (piece.horizontalSpanId != null) {
|
|
const spanId = String(piece.horizontalSpanId).replace(/[^\w-]+/g, '_');
|
|
const role = String(piece.horizontalSpanRole || 'panel').toUpperCase();
|
|
baseSlug = `HS_${spanId}_${role}_${baseSlug}`;
|
|
if (piece.horizontalSpanRole === 'door' && Number.isFinite(Number(piece.doorIndex))) {
|
|
baseSlug += `_${Number(piece.doorIndex) + 1}`;
|
|
}
|
|
}
|
|
|
|
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 || '';
|
|
const widthMm = toSafe(piece.rawCutWidthMm, piece.widthMm);
|
|
const heightMm = toSafe(piece.rawCutHeightMm, piece.heightMm);
|
|
const vertical = heightMm > widthMm;
|
|
if (vertical) {
|
|
return {
|
|
DRWIN: '270.0',
|
|
beschGedreht: '0',
|
|
turnMsg: '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 axis = Number(abst);
|
|
const start = vertical
|
|
? [Math.max(0, Lmm - axis), 0]
|
|
: [0, Math.max(0, Bmm - axis)];
|
|
const end = vertical
|
|
? [Math.max(0, Lmm - axis), Bmm]
|
|
: [Lmm, Math.max(0, Bmm - axis)];
|
|
|
|
return [
|
|
'<121 \\Block\\',
|
|
'MNM="Kantenbearbeitungen von oben"',
|
|
'DP="1"',
|
|
'KO="04"',
|
|
'<109 \\Nuten\\',
|
|
'MNM="Rainure dos"',
|
|
`XA="${fmtMm(start[0])}"`,
|
|
`YA="${fmtMm(start[1])}"`,
|
|
`XE="${fmtMm(end[0])}"`,
|
|
`YE="${fmtMm(end[1])}"`,
|
|
`NB="${gw}"`,
|
|
'RK="NOWRK"',
|
|
'EM="MOD2"',
|
|
'TV="0"',
|
|
'MV="GL"',
|
|
`TI="${gd}"`,
|
|
'MN="GL"',
|
|
'KO="04"'
|
|
];
|
|
}
|
|
|
|
function doorHandleGrooveSection(piece, L, B) {
|
|
const groove = piece?.machining?.fullHeightHandleGroove;
|
|
if (piece?.elementType !== 'portes' || !groove) return [];
|
|
const widthMm = Math.max(1, Number(groove.grooveWidthMm) || 8);
|
|
const depthMm = Math.max(0.1, Number(groove.grooveDepthMm) || 3);
|
|
const offsetMm = Math.max(0, Number(groove.topBottomOffsetMm) || 0);
|
|
const hingeSide = piece?.doorPlanHints?.hingeSide === 'right' ? 'right' : 'left';
|
|
const xMm = hingeSide === 'right' ? widthMm / 2 : L - widthMm / 2;
|
|
const y1Mm = Math.min(B, offsetMm);
|
|
const y2Mm = Math.max(y1Mm, B - offsetMm);
|
|
return [
|
|
'<109 \\Nuten\\',
|
|
'MNM="Rainure poignée toute hauteur"',
|
|
`XA="${fmtMm(xMm)}"`,
|
|
`YA="${fmtMm(y1Mm)}"`,
|
|
`XE="${fmtMm(xMm)}"`,
|
|
`YE="${fmtMm(y2Mm)}"`,
|
|
`NB="${fmtMm(widthMm)}"`,
|
|
'RK="NOWRK"',
|
|
'EM="MOD2"',
|
|
'TV="0"',
|
|
'MV="GL"',
|
|
`TI="${fmtMm(depthMm)}"`,
|
|
'MN="GL"',
|
|
'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;
|
|
const fromUnder = spec.face === 'back';
|
|
let bm = 'LS';
|
|
let ti;
|
|
if (throughHint || looksThrough) {
|
|
bm = fromUnder ? 'LSLU' : 'LSL';
|
|
ti = `${fmtMm(thicknessMm)} + 1`;
|
|
} else if (edgeHint && depthRaw >= 38) {
|
|
bm = 'LSL';
|
|
ti = fmtMm(depthRaw);
|
|
} else {
|
|
bm = fromUnder ? 'LSU' : 'LS';
|
|
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"'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Un trou de chant → lignes <103 BohrHoriz (perçage horizontal dans un chant).
|
|
* `end` désigne le chant d'entrée dans le repère pièce (origine bas-gauche) :
|
|
* left → depuis chant gauche (BM=XP), right → chant droit (BM=XM),
|
|
* bottom → chant avant Y=0 (BM=YP), top → chant arrière Y=B (BM=YM).
|
|
* ZA = position dans l'épaisseur (axe du trou) : `zMm` si fourni, sinon milieu (D/2).
|
|
*/
|
|
function bohrHorizLines(spec, L, B, D) {
|
|
const du = fmtMm(Math.round((Number(spec.diameterMm) || 8) * 10) / 10);
|
|
const ti = fmtMm(Number(spec.depthMm) || 25);
|
|
const za = fmtMm(Number.isFinite(Number(spec.zMm)) ? Number(spec.zMm) : D / 2);
|
|
|
|
let bm;
|
|
let xa;
|
|
let ya;
|
|
switch (spec.end) {
|
|
case 'right':
|
|
bm = 'XM';
|
|
xa = L;
|
|
ya = spec.yMm;
|
|
break;
|
|
case 'bottom':
|
|
bm = 'YP';
|
|
xa = spec.xMm;
|
|
ya = 0;
|
|
break;
|
|
case 'top':
|
|
bm = 'YM';
|
|
xa = spec.xMm;
|
|
ya = B;
|
|
break;
|
|
case 'left':
|
|
default:
|
|
bm = 'XP';
|
|
xa = 0;
|
|
ya = spec.yMm;
|
|
break;
|
|
}
|
|
|
|
return [
|
|
'<103 \\BohrHoriz\\',
|
|
`MNM="Bohrung ${du} mm"`,
|
|
`XA="${fmtMm(xa)}"`,
|
|
`YA="${fmtMm(ya)}"`,
|
|
`ZA="${za}"`,
|
|
`DU="${du}"`,
|
|
`TI="${ti}"`,
|
|
`BM="${bm}"`,
|
|
'AN="1"',
|
|
'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 &&
|
|
(o.face || 'face') === (s.face || 'face') &&
|
|
(o.end || '') === (s.end || '')
|
|
);
|
|
if (!hit) out.push(s);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* La fraise à rainurer travaille depuis le dessus de la machine. Pour le
|
|
* panneau de dessus, on pose donc la face intérieure caisse vers le haut :
|
|
* rainure dos + assemblages caisse dessus, perçages d'habillage dessous.
|
|
*/
|
|
function orientFaceSpecsForMpr(piece, specs) {
|
|
if (piece?.elementType !== 'dessus' || !piece?.machining?.groove) return specs;
|
|
return specs.map((spec) => {
|
|
if (spec.face === 'face') return { ...spec, face: 'back' };
|
|
if (spec.face === 'back') return { ...spec, face: 'face' };
|
|
return spec;
|
|
});
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Coupes d'onglet 45° (habillages de finition) :
|
|
* - tourillons perpendiculaires à la face d'onglet → <104 BohrUniv (perçage
|
|
* spatial : azimut C `CA` + inclinaison agrégat `WI`), sur le système 04.
|
|
* (<102 BohrVert exige un plan parallèle au plan supérieur.)
|
|
* - trait de scie 45° → <124 Nut_R (« Nuten im A-Winkel » : sciage avec lame
|
|
* inclinée WI, macro documenté du format MPR, équivalent Uni_Saeg_...W45
|
|
* du postprocesseur DXF HOMAG). Directement sur le plan supérieur (KO 04) :
|
|
* pas de plan incliné custom, donc affichage propre et génération NC
|
|
* possible avec l'agrégat scie pivotante.
|
|
* Géométrie (coordonnées pièce) : la pièce occupe Z=0..D. La scie démarre donc
|
|
* explicitement sur la face supérieure ZA=D, puis descend à 45° vers le chant
|
|
* extérieur. Les trajectoires sont parcourues de sorte que leur normale gauche
|
|
* pointe vers la chute : l'axe de scie est parallèle aux tourillons et le plan
|
|
* de lame leur est perpendiculaire. Aucun plan S auxiliaire n'est nécessaire.
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
const MITER_SAW_KERF_MM = 4;
|
|
|
|
function buildMiterSections({ miterSpecs, L, B, D }) {
|
|
const empty = { levelLines: [], opLines: [] };
|
|
if (!miterSpecs.length) return empty;
|
|
|
|
const kerfCenter = MITER_SAW_KERF_MM / 2;
|
|
|
|
// Trajectoires globales sur KO=04. Pour chaque chant, le côté gauche du
|
|
// parcours est l'extérieur de la pièce (la chute).
|
|
const SIDES = {
|
|
left: {
|
|
label: 'gauche',
|
|
ca: 0,
|
|
entry: (sp) => [D / 2, sp.yMm],
|
|
sawPath: [[D - kerfCenter, 0], [D - kerfCenter, B]]
|
|
},
|
|
right: {
|
|
label: 'droite',
|
|
ca: 180,
|
|
entry: (sp) => [L - D / 2, sp.yMm],
|
|
sawPath: [[L - D + kerfCenter, B], [L - D + kerfCenter, 0]]
|
|
},
|
|
top: {
|
|
label: 'haut',
|
|
ca: 270,
|
|
entry: (sp) => [sp.xMm, B - D / 2],
|
|
sawPath: [[0, B - D + kerfCenter], [L, B - D + kerfCenter]]
|
|
}
|
|
};
|
|
|
|
const opLines = [];
|
|
|
|
for (const [side, def] of Object.entries(SIDES)) {
|
|
const specs = miterSpecs.filter((sp) => sp.end === side);
|
|
if (!specs.length) continue;
|
|
|
|
/* Tourillons : perçage spatial sur le système 04 (repère pièce). */
|
|
for (const sp of specs) {
|
|
const [xa, ya] = def.entry(sp);
|
|
const du = fmtMm(Number(sp.diameterMm) || 8);
|
|
opLines.push(
|
|
'<104 \\BohrUniv\\',
|
|
`MNM="Tourillon onglet ${def.label} ${du} mm"`,
|
|
`XA="${fmtMm(xa)}"`,
|
|
`YA="${fmtMm(ya)}"`,
|
|
`ZA="${fmtMm(D / 2)}"`,
|
|
`CA="${def.ca}"`,
|
|
'WI="45"',
|
|
// woodWOP compte TI depuis le point XA/YA/ZA le long de l'axe incliné :
|
|
// avec l'entrée au centre de la face d'onglet, la profondeur affichée
|
|
// vaut le double de la profondeur utile → on émet la moitié.
|
|
`TI="${fmtMm((Number(sp.depthMm) || 15) / 2)}"`,
|
|
`DU="${du}"`,
|
|
'AN="1"',
|
|
'F_="STANDARD"',
|
|
'KO="04"'
|
|
);
|
|
}
|
|
|
|
/* Trait de scie 45° incliné (Nut_R / Nuten im A-Winkel). */
|
|
const [start, end] = def.sawPath;
|
|
opLines.push(
|
|
'<124 \\Nut_R\\',
|
|
`MNM="Coupe onglet 45 ${def.label}"`,
|
|
`XA="${fmtMm(start[0])}"`,
|
|
`YA="${fmtMm(start[1])}"`,
|
|
`XE="${fmtMm(end[0])}"`,
|
|
`YE="${fmtMm(end[1])}"`,
|
|
// KO=04 a son origine en Z=0 sous la pièce ; la face supérieure est Z=D.
|
|
`ZA="${fmtMm(D)}"`,
|
|
'WI="45"',
|
|
`NB="${fmtMm(MITER_SAW_KERF_MM)}"`,
|
|
'RK="NOWRK"',
|
|
'EM="MOD2"',
|
|
'TV="0"',
|
|
'MV="GL"',
|
|
`TI="${fmtMm(D * Math.SQRT2 + 2)}"`,
|
|
'MN="GL"',
|
|
'F_="STANDARD"',
|
|
'KO="04"'
|
|
);
|
|
}
|
|
|
|
return { levelLines: [], opLines };
|
|
}
|
|
|
|
export 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 = orientFaceSpecsForMpr(piece, dedupeHoleSpecs(specs));
|
|
|
|
// Répartition des perçages par destination d'usinage (repère pièce, origine bas-gauche) :
|
|
// - onglet 45° (habillages) → plans inclinés dédiés (buildMiterSections),
|
|
// - chant (face='end') → <103 BohrHoriz, regroupés par côté d'entrée,
|
|
// - face opposée (face='back') → <102 BohrVert avec cycle WoodWOP « von unten »,
|
|
// - grande face (défaut) → <102 BohrVert « von oben ».
|
|
const miterSpecs = unique.filter((sp) => sp.type === 'tourillon_miter' && sp.end);
|
|
const rest = unique.filter((sp) => !miterSpecs.includes(sp));
|
|
const edgeSpecs = rest.filter((sp) => sp.face === 'end');
|
|
const backSpecs = rest.filter((sp) => sp.face === 'back');
|
|
const topSpecs = rest.filter((sp) => sp.face !== 'end' && sp.face !== 'back');
|
|
|
|
const matKm = escQuoted(
|
|
piece.finishId != null && piece.finishId !== '' ? piece.finishId : defaultMaterialArticle || 'APEX'
|
|
);
|
|
|
|
// Les opérations restent exprimées dans le repère pièce KO=04. DRWIN tourne
|
|
// uniquement la présentation/pose machine (pièces hautes couchées en X).
|
|
const miter = buildMiterSections({ miterSpecs, L, B, D });
|
|
|
|
const bez = escQuoted(baseDesignation(slug));
|
|
const hasBack = backSpecs.length > 0;
|
|
|
|
const lines = [];
|
|
|
|
lines.push('[H', 'VERSION="4.0 Alpha"', 'FW="1400"', 'FM="1"', 'ZS="40"', 'OP="1"');
|
|
// Une pièce plus haute que large est couchée à 270° pour correspondre à
|
|
// l'orientation machine; les formules DW conservent son repère d'usinage.
|
|
lines.push('[001', 'MAT="2"', 'KM="Materialart"', `DRWIN="${angles.DRWIN}"`, 'KM="Drehwinkel 0,90,180,270"');
|
|
lines.push('AUFT="0"', `KM="AUFTRAG: ${escQuoted(orderReference)}"`);
|
|
lines.push('STKL="0"', `KM="STÜCKLISTE: ${escQuoted(stackListKm)}"`);
|
|
lines.push(`POS="${escQuoted(posLabel)}"`, `KM="POSITION: ${escQuoted(posLabel)}"`);
|
|
lines.push('BEZ="0"', `KM="Bezeichnung: ${bez}"`);
|
|
lines.push(`L="${fmtMm(L)}"`, 'KM="Länge in X"', `B="${fmtMm(B)}"`, 'KM="Breite in Y"', `D="${fmtMm(D)}"`, 'KM="Dicke in Z"');
|
|
lines.push('ZF="2.0"', 'KM="Zugabe für DurchgangsFräsung"', 'ZB="3.0"', 'KM="Zugabe für DurchgangsBohrung"');
|
|
lines.push('RITZEN="1.0"', 'KM="Vorritztiefe in mm - 0=nicht Vorritzen"');
|
|
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"', 'KM="Zugabe für Gehrung links"');
|
|
lines.push('ZUGRE="0.0"', 'KM="Zugabe für Gehrung rechts"');
|
|
lines.push('ZUGVO="0.0"', 'KM="Zugabe für Gehrung vorne"');
|
|
lines.push('ZUGHI="0.0"', 'KM="Zugabe für Gehrung hinten"');
|
|
lines.push('DW="IF DRWIN=0 OR DRWIN=90 OR DRWIN=180 THEN DRWIN ELSE 270"', 'KM="DISABLED Drehwinkel korrigiert bei Fehleingabe"');
|
|
lines.push('ld="IF DW=90 OR DW=270 THEN B ELSE L"', 'KM="DISABLED Länge in X (gedreht)"');
|
|
lines.push('bd="IF DW=90 OR DW=270 THEN L ELSE B"', 'KM="DISABLED Breite in Y (gedreht)"');
|
|
lines.push('offxL="IF DW=180 THEN ZUGRE ELSE ZUGLI"', 'KM="DISABLED Offset in X (0° oder 180°)"');
|
|
lines.push('offxB="IF DW=90 THEN ZUGHI ELSE ZUGVO"', 'KM="DISABLED Offset in X (90° oder 270°)"');
|
|
lines.push('offyL="IF DW=180 THEN ZUGHI ELSE ZUGVO"', 'KM="DISABLED Offset in Y (0° oder 180°)"');
|
|
lines.push('offyB="IF DW=90 THEN ZUGLI ELSE ZUGRE"', 'KM="DISABLED Offset in Y (90° oder 270°)"');
|
|
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"',
|
|
...miter.levelLines,
|
|
'<100 \\WerkStck\\',
|
|
// WerkStck uses the piece-local dimensions. KO=04 applies DRWIN to the
|
|
// stock and operations together; swapping here would rotate the stock twice.
|
|
`LA="${fmtMm(L)}"`,
|
|
`BR="${fmtMm(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="STÜCKLISTE : ${escQuoted(stackListKm)}"`,
|
|
`KM="POSITION : ${escQuoted(posLabel)}"`,
|
|
`KM="Bezeichnung: ${escQuoted(slug)}"`,
|
|
'KM=""',
|
|
`KM="${escQuoted(angles.turnMsg)}"`,
|
|
'KM=""',
|
|
'KM="Teileinfo"',
|
|
`KM="Größe: ${fmtMm(L)} x ${fmtMm(B)}"`,
|
|
`KM="Stärke: ${fmtMm(D)}"`,
|
|
`KM=""`,
|
|
`KM="Material: ${matKm}"`,
|
|
'KM="Belag IN: "',
|
|
'KM="Belag AU: "',
|
|
'KM=""'
|
|
);
|
|
|
|
// Les usinages de sous-face utilisent directement BM=LSU/LSLU et ne
|
|
// nécessitent donc aucun retournement de la pièce.
|
|
lines.push(
|
|
'<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"'
|
|
);
|
|
// Macro de formatage (RSO_Format) — exécutée seulement si FORMAT=1 (??="FORMAT").
|
|
lines.push(
|
|
'<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"'
|
|
);
|
|
|
|
// Bloc « Bearbeitungen von oben » — perçages verticaux sur la grande face.
|
|
if (topSpecs.length) {
|
|
lines.push('<121 \\Block\\', 'MNM="Bearbeitungen von oben "', `DP="${topSpecs.length}"`, 'KO="04"');
|
|
for (const sp of topSpecs) lines.push(...bohrVertLines(sp, thicknessMm));
|
|
}
|
|
|
|
// Bloc « Bearbeitungen von unten » — BM=LSU/LSLU indique réellement à
|
|
// WoodWOP que l'outil entre par la sous-face.
|
|
if (hasBack) {
|
|
lines.push('<121 \\Block\\', 'MNM="Bearbeitungen von unten "', `DP="${backSpecs.length}"`, 'KO="04"');
|
|
for (const sp of backSpecs) {
|
|
lines.push(...bohrVertLines(sp, thicknessMm));
|
|
}
|
|
}
|
|
|
|
// Blocs de perçage sur chant — un bloc par côté d'entrée.
|
|
const edgeBlocks = [
|
|
['left', 'Bearbeitungen von links '],
|
|
['right', 'Bearbeitungen von rechts '],
|
|
['bottom', 'Bearbeitungen von vorne '],
|
|
['top', 'Bearbeitungen von hinten ']
|
|
];
|
|
for (const [end, label] of edgeBlocks) {
|
|
const group = edgeSpecs.filter((sp) => (sp.end || 'left') === end);
|
|
if (!group.length) continue;
|
|
lines.push('<121 \\Block\\', `MNM="${label}"`, `DP="${group.length}"`, 'KO="04"');
|
|
for (const sp of group) lines.push(...bohrHorizLines(sp, L, B, D));
|
|
}
|
|
|
|
lines.push(...miter.opLines);
|
|
lines.push(...grooveSection(piece, L, B));
|
|
lines.push(...doorHandleGrooveSection(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) {
|
|
// Encodage Windows-1252 (Latin-1) : format attendu par woodWOP pour les
|
|
// commentaires KM allemands (ä ö ü ß °), comme dans les fichiers fabricant.
|
|
if (records.length === 1) {
|
|
const only = records[0];
|
|
return {
|
|
buffer: Buffer.from(only.content, 'latin1'),
|
|
fileName: only.fileName,
|
|
mimeType: 'application/octet-stream',
|
|
fileCount: 1
|
|
};
|
|
}
|
|
|
|
const zip = new JSZip();
|
|
for (const rec of records) {
|
|
zip.file(rec.fileName, Buffer.from(rec.content, 'latin1'));
|
|
}
|
|
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
|
|
};
|